feat: Add customizable URI template manager factory to MCP server
Implement URI template functionality for MCP resources, allowing dynamic
resource URIs with variables in the format {variableName}.
- Enable resource URIs with variable placeholders (e.g., "/api/users/{userId}")
- Automatic extraction of variable values from request URIs
- Validation of template arguments in completions
- Matching of request URIs against templates
- Add new URI template management interfaces and implementations
- Enhanced resource template listing to include templated resources
- Updated resource request handling to support template matching
- Test coverage for URI template functionality
- Adding a configurable uriTemplateManagerFactory field to both AsyncSpecification and SyncSpecification classes
- Adding builder methods to allow setting a custom URI template manager factory
- Modifying constructors to pass the URI template manager factory to the server implementation
- Updating the server implementation to use the provided factory
- Add bulk registration methods for async completions
Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
parent
261554bb7f
commit
e610d853f9
8 changed files with 489 additions and 16 deletions
|
|
@ -776,7 +776,8 @@ class WebFluxSseIntegrationTests {
|
|||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
||||
.capabilities(ServerCapabilities.builder().completions().build())
|
||||
.prompts(new McpServerFeatures.SyncPromptSpecification(
|
||||
new Prompt("code_review", "this is code review prompt", List.of()),
|
||||
new Prompt("code_review", "this is code review prompt",
|
||||
List.of(new PromptArgument("language", "string", false))),
|
||||
(mcpSyncServerExchange, getPromptRequest) -> null))
|
||||
.completions(new McpServerFeatures.SyncCompletionSpecification(
|
||||
new McpSchema.PromptReference("ref/prompt", "code_review"), completionHandler))
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
package io.modelcontextprotocol.server;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -22,10 +23,13 @@ import io.modelcontextprotocol.spec.McpSchema;
|
|||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
||||
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
|
||||
import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
||||
import io.modelcontextprotocol.spec.McpServerSession;
|
||||
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
||||
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
|
||||
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
|
||||
import io.modelcontextprotocol.util.Utils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
|
@ -92,8 +96,10 @@ public class McpAsyncServer {
|
|||
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
|
||||
*/
|
||||
McpAsyncServer(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
|
||||
McpServerFeatures.Async features, Duration requestTimeout) {
|
||||
this.delegate = new AsyncServerImpl(mcpTransportProvider, objectMapper, requestTimeout, features);
|
||||
McpServerFeatures.Async features, Duration requestTimeout,
|
||||
McpUriTemplateManagerFactory uriTemplateManagerFactory) {
|
||||
this.delegate = new AsyncServerImpl(mcpTransportProvider, objectMapper, requestTimeout, features,
|
||||
uriTemplateManagerFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -274,8 +280,11 @@ public class McpAsyncServer {
|
|||
|
||||
private List<String> protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
|
||||
|
||||
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
|
||||
|
||||
AsyncServerImpl(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
|
||||
Duration requestTimeout, McpServerFeatures.Async features) {
|
||||
Duration requestTimeout, McpServerFeatures.Async features,
|
||||
McpUriTemplateManagerFactory uriTemplateManagerFactory) {
|
||||
this.mcpTransportProvider = mcpTransportProvider;
|
||||
this.objectMapper = objectMapper;
|
||||
this.serverInfo = features.serverInfo();
|
||||
|
|
@ -286,6 +295,7 @@ public class McpAsyncServer {
|
|||
this.resourceTemplates.addAll(features.resourceTemplates());
|
||||
this.prompts.putAll(features.prompts());
|
||||
this.completions.putAll(features.completions());
|
||||
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
|
||||
|
||||
Map<String, McpServerSession.RequestHandler<?>> requestHandlers = new HashMap<>();
|
||||
|
||||
|
|
@ -564,21 +574,44 @@ public class McpAsyncServer {
|
|||
|
||||
private McpServerSession.RequestHandler<McpSchema.ListResourceTemplatesResult> resourceTemplateListRequestHandler() {
|
||||
return (exchange, params) -> Mono
|
||||
.just(new McpSchema.ListResourceTemplatesResult(this.resourceTemplates, null));
|
||||
.just(new McpSchema.ListResourceTemplatesResult(this.getResourceTemplates(), null));
|
||||
|
||||
}
|
||||
|
||||
private List<McpSchema.ResourceTemplate> getResourceTemplates() {
|
||||
var list = new ArrayList<>(this.resourceTemplates);
|
||||
List<ResourceTemplate> resourceTemplates = this.resources.keySet()
|
||||
.stream()
|
||||
.filter(uri -> uri.contains("{"))
|
||||
.map(uri -> {
|
||||
var resource = this.resources.get(uri).resource();
|
||||
var template = new McpSchema.ResourceTemplate(resource.uri(), resource.name(),
|
||||
resource.description(), resource.mimeType(), resource.annotations());
|
||||
return template;
|
||||
})
|
||||
.toList();
|
||||
|
||||
list.addAll(resourceTemplates);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private McpServerSession.RequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
|
||||
return (exchange, params) -> {
|
||||
McpSchema.ReadResourceRequest resourceRequest = objectMapper.convertValue(params,
|
||||
new TypeReference<McpSchema.ReadResourceRequest>() {
|
||||
});
|
||||
var resourceUri = resourceRequest.uri();
|
||||
McpServerFeatures.AsyncResourceSpecification specification = this.resources.get(resourceUri);
|
||||
if (specification != null) {
|
||||
return specification.readHandler().apply(exchange, resourceRequest);
|
||||
}
|
||||
return Mono.error(new McpError("Resource not found: " + resourceUri));
|
||||
|
||||
McpServerFeatures.AsyncResourceSpecification specification = this.resources.values()
|
||||
.stream()
|
||||
.filter(resourceSpecification -> this.uriTemplateManagerFactory
|
||||
.create(resourceSpecification.resource().uri())
|
||||
.matches(resourceUri))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new McpError("Resource not found: " + resourceUri));
|
||||
|
||||
return specification.readHandler().apply(exchange, resourceRequest);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -729,20 +762,38 @@ public class McpAsyncServer {
|
|||
|
||||
String type = request.ref().type();
|
||||
|
||||
String argumentName = request.argument().name();
|
||||
|
||||
// check if the referenced resource exists
|
||||
if (type.equals("ref/prompt") && request.ref() instanceof McpSchema.PromptReference promptReference) {
|
||||
McpServerFeatures.AsyncPromptSpecification prompt = this.prompts.get(promptReference.name());
|
||||
if (prompt == null) {
|
||||
McpServerFeatures.AsyncPromptSpecification promptSpec = this.prompts.get(promptReference.name());
|
||||
if (promptSpec == null) {
|
||||
return Mono.error(new McpError("Prompt not found: " + promptReference.name()));
|
||||
}
|
||||
if (!promptSpec.prompt()
|
||||
.arguments()
|
||||
.stream()
|
||||
.filter(arg -> arg.name().equals(argumentName))
|
||||
.findFirst()
|
||||
.isPresent()) {
|
||||
|
||||
return Mono.error(new McpError("Argument not found: " + argumentName));
|
||||
}
|
||||
}
|
||||
|
||||
if (type.equals("ref/resource")
|
||||
&& request.ref() instanceof McpSchema.ResourceReference resourceReference) {
|
||||
McpServerFeatures.AsyncResourceSpecification resource = this.resources.get(resourceReference.uri());
|
||||
if (resource == null) {
|
||||
McpServerFeatures.AsyncResourceSpecification resourceSpec = this.resources
|
||||
.get(resourceReference.uri());
|
||||
if (resourceSpec == null) {
|
||||
return Mono.error(new McpError("Resource not found: " + resourceReference.uri()));
|
||||
}
|
||||
if (!uriTemplateManagerFactory.create(resourceSpec.resource().uri())
|
||||
.getVariableNames()
|
||||
.contains(argumentName)) {
|
||||
return Mono.error(new McpError("Argument not found: " + argumentName));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
McpServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
|||
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
|
||||
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
||||
import io.modelcontextprotocol.util.Assert;
|
||||
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
|
||||
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
|
|
@ -156,6 +158,8 @@ public interface McpServer {
|
|||
|
||||
private final McpServerTransportProvider transportProvider;
|
||||
|
||||
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
|
||||
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
|
||||
|
|
@ -204,6 +208,19 @@ public interface McpServer {
|
|||
this.transportProvider = transportProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URI template manager factory to use for creating URI templates. This
|
||||
* allows for custom URI template parsing and variable extraction.
|
||||
* @param uriTemplateManagerFactory The factory to use. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if uriTemplateManagerFactory is null
|
||||
*/
|
||||
public AsyncSpecification uriTemplateManagerFactory(McpUriTemplateManagerFactory uriTemplateManagerFactory) {
|
||||
Assert.notNull(uriTemplateManagerFactory, "URI template manager factory must not be null");
|
||||
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the duration to wait for server responses before timing out requests. This
|
||||
* timeout applies to all requests made through the client, including tool calls,
|
||||
|
|
@ -517,6 +534,36 @@ public interface McpServer {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers multiple completions with their handlers using a List. This method is
|
||||
* useful when completions need to be added in bulk from a collection.
|
||||
* @param completions List of completion specifications. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if completions is null
|
||||
*/
|
||||
public AsyncSpecification completions(List<McpServerFeatures.AsyncCompletionSpecification> completions) {
|
||||
Assert.notNull(completions, "Completions list must not be null");
|
||||
for (McpServerFeatures.AsyncCompletionSpecification completion : completions) {
|
||||
this.completions.put(completion.referenceKey(), completion);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers multiple completions with their handlers using varargs. This method
|
||||
* is useful when completions are defined inline and added directly.
|
||||
* @param completions Array of completion specifications. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if completions is null
|
||||
*/
|
||||
public AsyncSpecification completions(McpServerFeatures.AsyncCompletionSpecification... completions) {
|
||||
Assert.notNull(completions, "Completions list must not be null");
|
||||
for (McpServerFeatures.AsyncCompletionSpecification completion : completions) {
|
||||
this.completions.put(completion.referenceKey(), completion);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a consumer that will be notified when the list of roots changes. This
|
||||
* is useful for updating resource availability dynamically, such as when new
|
||||
|
|
@ -587,7 +634,8 @@ public interface McpServer {
|
|||
this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers,
|
||||
this.instructions);
|
||||
var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
|
||||
return new McpAsyncServer(this.transportProvider, mapper, features, this.requestTimeout);
|
||||
return new McpAsyncServer(this.transportProvider, mapper, features, this.requestTimeout,
|
||||
this.uriTemplateManagerFactory);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -600,6 +648,8 @@ public interface McpServer {
|
|||
private static final McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server",
|
||||
"1.0.0");
|
||||
|
||||
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
|
||||
|
||||
private final McpServerTransportProvider transportProvider;
|
||||
|
||||
private ObjectMapper objectMapper;
|
||||
|
|
@ -650,6 +700,19 @@ public interface McpServer {
|
|||
this.transportProvider = transportProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URI template manager factory to use for creating URI templates. This
|
||||
* allows for custom URI template parsing and variable extraction.
|
||||
* @param uriTemplateManagerFactory The factory to use. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if uriTemplateManagerFactory is null
|
||||
*/
|
||||
public SyncSpecification uriTemplateManagerFactory(McpUriTemplateManagerFactory uriTemplateManagerFactory) {
|
||||
Assert.notNull(uriTemplateManagerFactory, "URI template manager factory must not be null");
|
||||
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the duration to wait for server responses before timing out requests. This
|
||||
* timeout applies to all requests made through the client, including tool calls,
|
||||
|
|
@ -1064,7 +1127,8 @@ public interface McpServer {
|
|||
this.rootsChangeHandlers, this.instructions);
|
||||
McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures);
|
||||
var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
|
||||
var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures, this.requestTimeout);
|
||||
var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures, this.requestTimeout,
|
||||
this.uriTemplateManagerFactory);
|
||||
|
||||
return new McpSyncServer(asyncServer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright 2025 - 2025 the original author or authors.
|
||||
*/
|
||||
package io.modelcontextprotocol.util;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class DeafaultMcpUriTemplateManagerFactory implements McpUriTemplateManagerFactory {
|
||||
|
||||
/**
|
||||
* Creates a new instance of {@link McpUriTemplateManager} with the specified URI
|
||||
* template.
|
||||
* @param uriTemplate The URI template to be used for variable extraction
|
||||
* @return A new instance of {@link McpUriTemplateManager}
|
||||
* @throws IllegalArgumentException if the URI template is null or empty
|
||||
*/
|
||||
@Override
|
||||
public McpUriTemplateManager create(String uriTemplate) {
|
||||
return new DefaultMcpUriTemplateManager(uriTemplate);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* Copyright 2025-2025 the original author or authors.
|
||||
*/
|
||||
|
||||
package io.modelcontextprotocol.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Default implementation of the UriTemplateUtils interface.
|
||||
* <p>
|
||||
* This class provides methods for extracting variables from URI templates and matching
|
||||
* them against actual URIs.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class DefaultMcpUriTemplateManager implements McpUriTemplateManager {
|
||||
|
||||
/**
|
||||
* Pattern to match URI variables in the format {variableName}.
|
||||
*/
|
||||
private static final Pattern URI_VARIABLE_PATTERN = Pattern.compile("\\{([^/]+?)\\}");
|
||||
|
||||
private final String uriTemplate;
|
||||
|
||||
/**
|
||||
* Constructor for DefaultMcpUriTemplateManager.
|
||||
* @param uriTemplate The URI template to be used for variable extraction
|
||||
*/
|
||||
public DefaultMcpUriTemplateManager(String uriTemplate) {
|
||||
if (uriTemplate == null || uriTemplate.isEmpty()) {
|
||||
throw new IllegalArgumentException("URI template must not be null or empty");
|
||||
}
|
||||
this.uriTemplate = uriTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract URI variable names from a URI template.
|
||||
* @param uriTemplate The URI template containing variables in the format
|
||||
* {variableName}
|
||||
* @return A list of variable names extracted from the template
|
||||
* @throws IllegalArgumentException if duplicate variable names are found
|
||||
*/
|
||||
@Override
|
||||
public List<String> getVariableNames() {
|
||||
if (uriTemplate == null || uriTemplate.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> variables = new ArrayList<>();
|
||||
Matcher matcher = URI_VARIABLE_PATTERN.matcher(this.uriTemplate);
|
||||
|
||||
while (matcher.find()) {
|
||||
String variableName = matcher.group(1);
|
||||
if (variables.contains(variableName)) {
|
||||
throw new IllegalArgumentException("Duplicate URI variable name in template: " + variableName);
|
||||
}
|
||||
variables.add(variableName);
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract URI variable values from the actual request URI.
|
||||
* <p>
|
||||
* This method converts the URI template into a regex pattern, then uses that pattern
|
||||
* to extract variable values from the request URI.
|
||||
* @param requestUri The actual URI from the request
|
||||
* @return A map of variable names to their values
|
||||
* @throws IllegalArgumentException if the URI template is invalid or the request URI
|
||||
* doesn't match the template pattern
|
||||
*/
|
||||
@Override
|
||||
public Map<String, String> extractVariableValues(String requestUri) {
|
||||
Map<String, String> variableValues = new HashMap<>();
|
||||
List<String> uriVariables = this.getVariableNames();
|
||||
|
||||
if (requestUri == null || uriVariables.isEmpty()) {
|
||||
return variableValues;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create a regex pattern by replacing each {variableName} with a capturing
|
||||
// group
|
||||
StringBuilder patternBuilder = new StringBuilder("^");
|
||||
|
||||
// Find all variable placeholders and their positions
|
||||
Matcher variableMatcher = URI_VARIABLE_PATTERN.matcher(uriTemplate);
|
||||
int lastEnd = 0;
|
||||
|
||||
while (variableMatcher.find()) {
|
||||
// Add the text between the last variable and this one, escaped for regex
|
||||
String textBefore = uriTemplate.substring(lastEnd, variableMatcher.start());
|
||||
patternBuilder.append(Pattern.quote(textBefore));
|
||||
|
||||
// Add a capturing group for the variable
|
||||
patternBuilder.append("([^/]+)");
|
||||
|
||||
lastEnd = variableMatcher.end();
|
||||
}
|
||||
|
||||
// Add any remaining text after the last variable
|
||||
if (lastEnd < uriTemplate.length()) {
|
||||
patternBuilder.append(Pattern.quote(uriTemplate.substring(lastEnd)));
|
||||
}
|
||||
|
||||
patternBuilder.append("$");
|
||||
|
||||
// Compile the pattern and match against the request URI
|
||||
Pattern pattern = Pattern.compile(patternBuilder.toString());
|
||||
Matcher matcher = pattern.matcher(requestUri);
|
||||
|
||||
if (matcher.find() && matcher.groupCount() == uriVariables.size()) {
|
||||
for (int i = 0; i < uriVariables.size(); i++) {
|
||||
String value = matcher.group(i + 1);
|
||||
if (value == null || value.isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Empty value for URI variable '" + uriVariables.get(i) + "' in URI: " + requestUri);
|
||||
}
|
||||
variableValues.put(uriVariables.get(i), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalArgumentException("Error parsing URI template: " + uriTemplate + " for URI: " + requestUri,
|
||||
e);
|
||||
}
|
||||
|
||||
return variableValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a URI matches the uriTemplate with variables.
|
||||
* @param uri The URI to check
|
||||
* @return true if the URI matches the pattern, false otherwise
|
||||
*/
|
||||
@Override
|
||||
public boolean matches(String uri) {
|
||||
// If the uriTemplate doesn't contain variables, do a direct comparison
|
||||
if (!this.isUriTemplate(this.uriTemplate)) {
|
||||
return uri.equals(this.uriTemplate);
|
||||
}
|
||||
|
||||
// Convert the pattern to a regex
|
||||
String regex = this.uriTemplate.replaceAll("\\{[^/]+?\\}", "([^/]+?)");
|
||||
regex = regex.replace("/", "\\/");
|
||||
|
||||
// Check if the URI matches the regex
|
||||
return Pattern.compile(regex).matcher(uri).matches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUriTemplate(String uri) {
|
||||
return URI_VARIABLE_PATTERN.matcher(uri).find();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright 2025-2025 the original author or authors.
|
||||
*/
|
||||
|
||||
package io.modelcontextprotocol.util;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Interface for working with URI templates.
|
||||
* <p>
|
||||
* This interface provides methods for extracting variables from URI templates and
|
||||
* matching them against actual URIs.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public interface McpUriTemplateManager {
|
||||
|
||||
/**
|
||||
* Extract URI variable names from this URI template.
|
||||
* @return A list of variable names extracted from the template
|
||||
* @throws IllegalArgumentException if duplicate variable names are found
|
||||
*/
|
||||
List<String> getVariableNames();
|
||||
|
||||
/**
|
||||
* Extract URI variable values from the actual request URI.
|
||||
* <p>
|
||||
* This method converts the URI template into a regex pattern, then uses that pattern
|
||||
* to extract variable values from the request URI.
|
||||
* @param uri The actual URI from the request
|
||||
* @return A map of variable names to their values
|
||||
* @throws IllegalArgumentException if the URI template is invalid or the request URI
|
||||
* doesn't match the template pattern
|
||||
*/
|
||||
Map<String, String> extractVariableValues(String uri);
|
||||
|
||||
/**
|
||||
* Indicate whether the given URI matches this template.
|
||||
* @param uri the URI to match to
|
||||
* @return {@code true} if it matches; {@code false} otherwise
|
||||
*/
|
||||
boolean matches(String uri);
|
||||
|
||||
/**
|
||||
* Check if the given URI is a URI template.
|
||||
* @return Returns true if the URI contains variables in the format {variableName}
|
||||
*/
|
||||
public boolean isUriTemplate(String uri);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2025 - 2025 the original author or authors.
|
||||
*/
|
||||
package io.modelcontextprotocol.util;
|
||||
|
||||
/**
|
||||
* Factory interface for creating instances of {@link McpUriTemplateManager}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public interface McpUriTemplateManagerFactory {
|
||||
|
||||
/**
|
||||
* Creates a new instance of {@link McpUriTemplateManager} with the specified URI
|
||||
* template.
|
||||
* @param uriTemplate The URI template to be used for variable extraction
|
||||
* @return A new instance of {@link McpUriTemplateManager}
|
||||
* @throws IllegalArgumentException if the URI template is null or empty
|
||||
*/
|
||||
McpUriTemplateManager create(String uriTemplate);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright 2025-2025 the original author or authors.
|
||||
*/
|
||||
|
||||
package io.modelcontextprotocol;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
|
||||
import io.modelcontextprotocol.util.McpUriTemplateManager;
|
||||
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for {@link McpUriTemplateManager} and its implementations.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class McpUriTemplateManagerTests {
|
||||
|
||||
private McpUriTemplateManagerFactory uriTemplateFactory;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.uriTemplateFactory = new DeafaultMcpUriTemplateManagerFactory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExtractVariableNamesFromTemplate() {
|
||||
List<String> variables = this.uriTemplateFactory.create("/api/users/{userId}/posts/{postId}")
|
||||
.getVariableNames();
|
||||
assertEquals(2, variables.size());
|
||||
assertEquals("userId", variables.get(0));
|
||||
assertEquals("postId", variables.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyListWhenTemplateHasNoVariables() {
|
||||
List<String> variables = this.uriTemplateFactory.create("/api/users/all").getVariableNames();
|
||||
assertEquals(0, variables.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenExtractingVariablesFromNullTemplate() {
|
||||
assertThrows(IllegalArgumentException.class, () -> this.uriTemplateFactory.create(null).getVariableNames());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenExtractingVariablesFromEmptyTemplate() {
|
||||
assertThrows(IllegalArgumentException.class, () -> this.uriTemplateFactory.create("").getVariableNames());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenTemplateContainsDuplicateVariables() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> this.uriTemplateFactory.create("/api/users/{userId}/posts/{userId}").getVariableNames());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExtractVariableValuesFromRequestUri() {
|
||||
Map<String, String> values = this.uriTemplateFactory.create("/api/users/{userId}/posts/{postId}")
|
||||
.extractVariableValues("/api/users/123/posts/456");
|
||||
assertEquals(2, values.size());
|
||||
assertEquals("123", values.get("userId"));
|
||||
assertEquals("456", values.get("postId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyMapWhenTemplateHasNoVariables() {
|
||||
Map<String, String> values = this.uriTemplateFactory.create("/api/users/all")
|
||||
.extractVariableValues("/api/users/all");
|
||||
assertEquals(0, values.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyMapWhenRequestUriIsNull() {
|
||||
Map<String, String> values = this.uriTemplateFactory.create("/api/users/{userId}/posts/{postId}")
|
||||
.extractVariableValues(null);
|
||||
assertEquals(0, values.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchUriAgainstTemplatePattern() {
|
||||
var uriTemplateManager = this.uriTemplateFactory.create("/api/users/{userId}/posts/{postId}");
|
||||
|
||||
assertTrue(uriTemplateManager.matches("/api/users/123/posts/456"));
|
||||
assertFalse(uriTemplateManager.matches("/api/users/123/comments/456"));
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue