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)
|
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
||||||
.capabilities(ServerCapabilities.builder().completions().build())
|
.capabilities(ServerCapabilities.builder().completions().build())
|
||||||
.prompts(new McpServerFeatures.SyncPromptSpecification(
|
.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))
|
(mcpSyncServerExchange, getPromptRequest) -> null))
|
||||||
.completions(new McpServerFeatures.SyncCompletionSpecification(
|
.completions(new McpServerFeatures.SyncCompletionSpecification(
|
||||||
new McpSchema.PromptReference("ref/prompt", "code_review"), completionHandler))
|
new McpSchema.PromptReference("ref/prompt", "code_review"), completionHandler))
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
package io.modelcontextprotocol.server;
|
package io.modelcontextprotocol.server;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -22,10 +23,13 @@ import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest;
|
import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
||||||
import io.modelcontextprotocol.spec.McpServerSession;
|
import io.modelcontextprotocol.spec.McpServerSession;
|
||||||
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
||||||
|
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
|
||||||
|
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
|
||||||
import io.modelcontextprotocol.util.Utils;
|
import io.modelcontextprotocol.util.Utils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
@ -92,8 +96,10 @@ public class McpAsyncServer {
|
||||||
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
|
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
|
||||||
*/
|
*/
|
||||||
McpAsyncServer(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
|
McpAsyncServer(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
|
||||||
McpServerFeatures.Async features, Duration requestTimeout) {
|
McpServerFeatures.Async features, Duration requestTimeout,
|
||||||
this.delegate = new AsyncServerImpl(mcpTransportProvider, objectMapper, requestTimeout, features);
|
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 List<String> protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
|
||||||
|
|
||||||
|
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
|
||||||
|
|
||||||
AsyncServerImpl(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
|
AsyncServerImpl(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
|
||||||
Duration requestTimeout, McpServerFeatures.Async features) {
|
Duration requestTimeout, McpServerFeatures.Async features,
|
||||||
|
McpUriTemplateManagerFactory uriTemplateManagerFactory) {
|
||||||
this.mcpTransportProvider = mcpTransportProvider;
|
this.mcpTransportProvider = mcpTransportProvider;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.serverInfo = features.serverInfo();
|
this.serverInfo = features.serverInfo();
|
||||||
|
|
@ -286,6 +295,7 @@ public class McpAsyncServer {
|
||||||
this.resourceTemplates.addAll(features.resourceTemplates());
|
this.resourceTemplates.addAll(features.resourceTemplates());
|
||||||
this.prompts.putAll(features.prompts());
|
this.prompts.putAll(features.prompts());
|
||||||
this.completions.putAll(features.completions());
|
this.completions.putAll(features.completions());
|
||||||
|
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
|
||||||
|
|
||||||
Map<String, McpServerSession.RequestHandler<?>> requestHandlers = new HashMap<>();
|
Map<String, McpServerSession.RequestHandler<?>> requestHandlers = new HashMap<>();
|
||||||
|
|
||||||
|
|
@ -564,21 +574,44 @@ public class McpAsyncServer {
|
||||||
|
|
||||||
private McpServerSession.RequestHandler<McpSchema.ListResourceTemplatesResult> resourceTemplateListRequestHandler() {
|
private McpServerSession.RequestHandler<McpSchema.ListResourceTemplatesResult> resourceTemplateListRequestHandler() {
|
||||||
return (exchange, params) -> Mono
|
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() {
|
private McpServerSession.RequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
|
||||||
return (exchange, params) -> {
|
return (exchange, params) -> {
|
||||||
McpSchema.ReadResourceRequest resourceRequest = objectMapper.convertValue(params,
|
McpSchema.ReadResourceRequest resourceRequest = objectMapper.convertValue(params,
|
||||||
new TypeReference<McpSchema.ReadResourceRequest>() {
|
new TypeReference<McpSchema.ReadResourceRequest>() {
|
||||||
});
|
});
|
||||||
var resourceUri = resourceRequest.uri();
|
var resourceUri = resourceRequest.uri();
|
||||||
McpServerFeatures.AsyncResourceSpecification specification = this.resources.get(resourceUri);
|
|
||||||
if (specification != null) {
|
McpServerFeatures.AsyncResourceSpecification specification = this.resources.values()
|
||||||
return specification.readHandler().apply(exchange, resourceRequest);
|
.stream()
|
||||||
}
|
.filter(resourceSpecification -> this.uriTemplateManagerFactory
|
||||||
return Mono.error(new McpError("Resource not found: " + resourceUri));
|
.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 type = request.ref().type();
|
||||||
|
|
||||||
|
String argumentName = request.argument().name();
|
||||||
|
|
||||||
// check if the referenced resource exists
|
// check if the referenced resource exists
|
||||||
if (type.equals("ref/prompt") && request.ref() instanceof McpSchema.PromptReference promptReference) {
|
if (type.equals("ref/prompt") && request.ref() instanceof McpSchema.PromptReference promptReference) {
|
||||||
McpServerFeatures.AsyncPromptSpecification prompt = this.prompts.get(promptReference.name());
|
McpServerFeatures.AsyncPromptSpecification promptSpec = this.prompts.get(promptReference.name());
|
||||||
if (prompt == null) {
|
if (promptSpec == null) {
|
||||||
return Mono.error(new McpError("Prompt not found: " + promptReference.name()));
|
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")
|
if (type.equals("ref/resource")
|
||||||
&& request.ref() instanceof McpSchema.ResourceReference resourceReference) {
|
&& request.ref() instanceof McpSchema.ResourceReference resourceReference) {
|
||||||
McpServerFeatures.AsyncResourceSpecification resource = this.resources.get(resourceReference.uri());
|
McpServerFeatures.AsyncResourceSpecification resourceSpec = this.resources
|
||||||
if (resource == null) {
|
.get(resourceReference.uri());
|
||||||
|
if (resourceSpec == null) {
|
||||||
return Mono.error(new McpError("Resource not found: " + resourceReference.uri()));
|
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());
|
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.McpSchema.ResourceTemplate;
|
||||||
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
||||||
import io.modelcontextprotocol.util.Assert;
|
import io.modelcontextprotocol.util.Assert;
|
||||||
|
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
|
||||||
|
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -156,6 +158,8 @@ public interface McpServer {
|
||||||
|
|
||||||
private final McpServerTransportProvider transportProvider;
|
private final McpServerTransportProvider transportProvider;
|
||||||
|
|
||||||
|
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
|
||||||
|
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
|
private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
|
||||||
|
|
@ -204,6 +208,19 @@ public interface McpServer {
|
||||||
this.transportProvider = transportProvider;
|
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
|
* 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,
|
* timeout applies to all requests made through the client, including tool calls,
|
||||||
|
|
@ -517,6 +534,36 @@ public interface McpServer {
|
||||||
return this;
|
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
|
* 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
|
* 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.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers,
|
||||||
this.instructions);
|
this.instructions);
|
||||||
var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
|
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",
|
private static final McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server",
|
||||||
"1.0.0");
|
"1.0.0");
|
||||||
|
|
||||||
|
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
|
||||||
|
|
||||||
private final McpServerTransportProvider transportProvider;
|
private final McpServerTransportProvider transportProvider;
|
||||||
|
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
|
|
@ -650,6 +700,19 @@ public interface McpServer {
|
||||||
this.transportProvider = transportProvider;
|
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
|
* 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,
|
* timeout applies to all requests made through the client, including tool calls,
|
||||||
|
|
@ -1064,7 +1127,8 @@ public interface McpServer {
|
||||||
this.rootsChangeHandlers, this.instructions);
|
this.rootsChangeHandlers, this.instructions);
|
||||||
McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures);
|
McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures);
|
||||||
var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
|
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);
|
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