Skip to content

Commit f9d154e

Browse files
committed
feat: Add URI template support for MCP resources
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 Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
1 parent 261554b commit f9d154e

File tree

7 files changed

+416
-11
lines changed

7 files changed

+416
-11
lines changed

mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,8 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) {
776776
var mcpServer = McpServer.sync(mcpServerTransportProvider)
777777
.capabilities(ServerCapabilities.builder().completions().build())
778778
.prompts(new McpServerFeatures.SyncPromptSpecification(
779-
new Prompt("code_review", "this is code review prompt", List.of()),
779+
new Prompt("code_review", "this is code review prompt",
780+
List.of(new PromptArgument("language", "string", false))),
780781
(mcpSyncServerExchange, getPromptRequest) -> null))
781782
.completions(new McpServerFeatures.SyncCompletionSpecification(
782783
new McpSchema.PromptReference("ref/prompt", "code_review"), completionHandler))

mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package io.modelcontextprotocol.server;
66

77
import java.time.Duration;
8+
import java.util.ArrayList;
89
import java.util.HashMap;
910
import java.util.List;
1011
import java.util.Map;
@@ -22,10 +23,13 @@
2223
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
2324
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
2425
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
26+
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
2527
import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest;
2628
import io.modelcontextprotocol.spec.McpSchema.Tool;
2729
import io.modelcontextprotocol.spec.McpServerSession;
2830
import io.modelcontextprotocol.spec.McpServerTransportProvider;
31+
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
32+
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
2933
import io.modelcontextprotocol.util.Utils;
3034
import org.slf4j.Logger;
3135
import org.slf4j.LoggerFactory;
@@ -274,6 +278,8 @@ private static class AsyncServerImpl extends McpAsyncServer {
274278

275279
private List<String> protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
276280

281+
private final McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
282+
277283
AsyncServerImpl(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
278284
Duration requestTimeout, McpServerFeatures.Async features) {
279285
this.mcpTransportProvider = mcpTransportProvider;
@@ -564,8 +570,26 @@ private McpServerSession.RequestHandler<McpSchema.ListResourcesResult> resources
564570

565571
private McpServerSession.RequestHandler<McpSchema.ListResourceTemplatesResult> resourceTemplateListRequestHandler() {
566572
return (exchange, params) -> Mono
567-
.just(new McpSchema.ListResourceTemplatesResult(this.resourceTemplates, null));
573+
.just(new McpSchema.ListResourceTemplatesResult(this.getResourceTemplates(), null));
574+
575+
}
568576

577+
private List<McpSchema.ResourceTemplate> getResourceTemplates() {
578+
var list = new ArrayList<>(this.resourceTemplates);
579+
List<ResourceTemplate> resourceTemplates = this.resources.keySet()
580+
.stream()
581+
.filter(uri -> uri.contains("{"))
582+
.map(uri -> {
583+
var resource = this.resources.get(uri).resource();
584+
var template = new McpSchema.ResourceTemplate(resource.uri(), resource.name(),
585+
resource.description(), resource.mimeType(), resource.annotations());
586+
return template;
587+
})
588+
.toList();
589+
590+
list.addAll(resourceTemplates);
591+
592+
return list;
569593
}
570594

571595
private McpServerSession.RequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
@@ -574,11 +598,16 @@ private McpServerSession.RequestHandler<McpSchema.ReadResourceResult> resourcesR
574598
new TypeReference<McpSchema.ReadResourceRequest>() {
575599
});
576600
var resourceUri = resourceRequest.uri();
577-
McpServerFeatures.AsyncResourceSpecification specification = this.resources.get(resourceUri);
578-
if (specification != null) {
579-
return specification.readHandler().apply(exchange, resourceRequest);
580-
}
581-
return Mono.error(new McpError("Resource not found: " + resourceUri));
601+
602+
McpServerFeatures.AsyncResourceSpecification specification = this.resources.values()
603+
.stream()
604+
.filter(resourceSpecification -> this.uriTemplateManagerFactory
605+
.create(resourceSpecification.resource().uri())
606+
.matches(resourceUri))
607+
.findFirst()
608+
.orElseThrow(() -> new McpError("Resource not found: " + resourceUri));
609+
610+
return specification.readHandler().apply(exchange, resourceRequest);
582611
};
583612
}
584613

@@ -729,20 +758,38 @@ private McpServerSession.RequestHandler<McpSchema.CompleteResult> completionComp
729758

730759
String type = request.ref().type();
731760

761+
String argumentName = request.argument().name();
762+
732763
// check if the referenced resource exists
733764
if (type.equals("ref/prompt") && request.ref() instanceof McpSchema.PromptReference promptReference) {
734-
McpServerFeatures.AsyncPromptSpecification prompt = this.prompts.get(promptReference.name());
735-
if (prompt == null) {
765+
McpServerFeatures.AsyncPromptSpecification promptSpec = this.prompts.get(promptReference.name());
766+
if (promptSpec == null) {
736767
return Mono.error(new McpError("Prompt not found: " + promptReference.name()));
737768
}
769+
if (!promptSpec.prompt()
770+
.arguments()
771+
.stream()
772+
.filter(arg -> arg.name().equals(argumentName))
773+
.findFirst()
774+
.isPresent()) {
775+
776+
return Mono.error(new McpError("Argument not found: " + argumentName));
777+
}
738778
}
739779

740780
if (type.equals("ref/resource")
741781
&& request.ref() instanceof McpSchema.ResourceReference resourceReference) {
742-
McpServerFeatures.AsyncResourceSpecification resource = this.resources.get(resourceReference.uri());
743-
if (resource == null) {
782+
McpServerFeatures.AsyncResourceSpecification resourceSpec = this.resources
783+
.get(resourceReference.uri());
784+
if (resourceSpec == null) {
744785
return Mono.error(new McpError("Resource not found: " + resourceReference.uri()));
745786
}
787+
if (!uriTemplateManagerFactory.create(resourceSpec.resource().uri())
788+
.getVariableNames()
789+
.contains(argumentName)) {
790+
return Mono.error(new McpError("Argument not found: " + argumentName));
791+
}
792+
746793
}
747794

748795
McpServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2025 - 2025 the original author or authors.
3+
*/
4+
package io.modelcontextprotocol.util;
5+
6+
/**
7+
* @author Christian Tzolov
8+
*/
9+
public class DeafaultMcpUriTemplateManagerFactory implements McpUriTemplateManagerFactory {
10+
11+
/**
12+
* Creates a new instance of {@link McpUriTemplateManager} with the specified URI
13+
* template.
14+
* @param uriTemplate The URI template to be used for variable extraction
15+
* @return A new instance of {@link McpUriTemplateManager}
16+
* @throws IllegalArgumentException if the URI template is null or empty
17+
*/
18+
@Override
19+
public McpUriTemplateManager create(String uriTemplate) {
20+
return new DefaultMcpUriTemplateManager(uriTemplate);
21+
}
22+
23+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.util;
6+
7+
import java.util.ArrayList;
8+
import java.util.HashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.regex.Matcher;
12+
import java.util.regex.Pattern;
13+
14+
/**
15+
* Default implementation of the UriTemplateUtils interface.
16+
* <p>
17+
* This class provides methods for extracting variables from URI templates and matching
18+
* them against actual URIs.
19+
*
20+
* @author Christian Tzolov
21+
*/
22+
public class DefaultMcpUriTemplateManager implements McpUriTemplateManager {
23+
24+
/**
25+
* Pattern to match URI variables in the format {variableName}.
26+
*/
27+
private static final Pattern URI_VARIABLE_PATTERN = Pattern.compile("\\{([^/]+?)\\}");
28+
29+
private final String uriTemplate;
30+
31+
/**
32+
* Constructor for DefaultMcpUriTemplateManager.
33+
* @param uriTemplate The URI template to be used for variable extraction
34+
*/
35+
public DefaultMcpUriTemplateManager(String uriTemplate) {
36+
if (uriTemplate == null || uriTemplate.isEmpty()) {
37+
throw new IllegalArgumentException("URI template must not be null or empty");
38+
}
39+
this.uriTemplate = uriTemplate;
40+
}
41+
42+
/**
43+
* Extract URI variable names from a URI template.
44+
* @param uriTemplate The URI template containing variables in the format
45+
* {variableName}
46+
* @return A list of variable names extracted from the template
47+
* @throws IllegalArgumentException if duplicate variable names are found
48+
*/
49+
@Override
50+
public List<String> getVariableNames() {
51+
if (uriTemplate == null || uriTemplate.isEmpty()) {
52+
return List.of();
53+
}
54+
55+
List<String> variables = new ArrayList<>();
56+
Matcher matcher = URI_VARIABLE_PATTERN.matcher(this.uriTemplate);
57+
58+
while (matcher.find()) {
59+
String variableName = matcher.group(1);
60+
if (variables.contains(variableName)) {
61+
throw new IllegalArgumentException("Duplicate URI variable name in template: " + variableName);
62+
}
63+
variables.add(variableName);
64+
}
65+
66+
return variables;
67+
}
68+
69+
/**
70+
* Extract URI variable values from the actual request URI.
71+
* <p>
72+
* This method converts the URI template into a regex pattern, then uses that pattern
73+
* to extract variable values from the request URI.
74+
* @param requestUri The actual URI from the request
75+
* @return A map of variable names to their values
76+
* @throws IllegalArgumentException if the URI template is invalid or the request URI
77+
* doesn't match the template pattern
78+
*/
79+
@Override
80+
public Map<String, String> extractVariableValues(String requestUri) {
81+
Map<String, String> variableValues = new HashMap<>();
82+
List<String> uriVariables = this.getVariableNames();
83+
84+
if (requestUri == null || uriVariables.isEmpty()) {
85+
return variableValues;
86+
}
87+
88+
try {
89+
// Create a regex pattern by replacing each {variableName} with a capturing
90+
// group
91+
StringBuilder patternBuilder = new StringBuilder("^");
92+
93+
// Find all variable placeholders and their positions
94+
Matcher variableMatcher = URI_VARIABLE_PATTERN.matcher(uriTemplate);
95+
int lastEnd = 0;
96+
97+
while (variableMatcher.find()) {
98+
// Add the text between the last variable and this one, escaped for regex
99+
String textBefore = uriTemplate.substring(lastEnd, variableMatcher.start());
100+
patternBuilder.append(Pattern.quote(textBefore));
101+
102+
// Add a capturing group for the variable
103+
patternBuilder.append("([^/]+)");
104+
105+
lastEnd = variableMatcher.end();
106+
}
107+
108+
// Add any remaining text after the last variable
109+
if (lastEnd < uriTemplate.length()) {
110+
patternBuilder.append(Pattern.quote(uriTemplate.substring(lastEnd)));
111+
}
112+
113+
patternBuilder.append("$");
114+
115+
// Compile the pattern and match against the request URI
116+
Pattern pattern = Pattern.compile(patternBuilder.toString());
117+
Matcher matcher = pattern.matcher(requestUri);
118+
119+
if (matcher.find() && matcher.groupCount() == uriVariables.size()) {
120+
for (int i = 0; i < uriVariables.size(); i++) {
121+
String value = matcher.group(i + 1);
122+
if (value == null || value.isEmpty()) {
123+
throw new IllegalArgumentException(
124+
"Empty value for URI variable '" + uriVariables.get(i) + "' in URI: " + requestUri);
125+
}
126+
variableValues.put(uriVariables.get(i), value);
127+
}
128+
}
129+
}
130+
catch (Exception e) {
131+
throw new IllegalArgumentException("Error parsing URI template: " + uriTemplate + " for URI: " + requestUri,
132+
e);
133+
}
134+
135+
return variableValues;
136+
}
137+
138+
/**
139+
* Check if a URI matches the uriTemplate with variables.
140+
* @param uri The URI to check
141+
* @return true if the URI matches the pattern, false otherwise
142+
*/
143+
@Override
144+
public boolean matches(String uri) {
145+
// If the uriTemplate doesn't contain variables, do a direct comparison
146+
if (!this.isUriTemplate(this.uriTemplate)) {
147+
return uri.equals(this.uriTemplate);
148+
}
149+
150+
// Convert the pattern to a regex
151+
String regex = this.uriTemplate.replaceAll("\\{[^/]+?\\}", "([^/]+?)");
152+
regex = regex.replace("/", "\\/");
153+
154+
// Check if the URI matches the regex
155+
return Pattern.compile(regex).matcher(uri).matches();
156+
}
157+
158+
@Override
159+
public boolean isUriTemplate(String uri) {
160+
return URI_VARIABLE_PATTERN.matcher(uri).find();
161+
}
162+
163+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.util;
6+
7+
import java.util.List;
8+
import java.util.Map;
9+
10+
/**
11+
* Interface for working with URI templates.
12+
* <p>
13+
* This interface provides methods for extracting variables from URI templates and
14+
* matching them against actual URIs.
15+
*
16+
* @author Christian Tzolov
17+
*/
18+
public interface McpUriTemplateManager {
19+
20+
/**
21+
* Extract URI variable names from this URI template.
22+
* @return A list of variable names extracted from the template
23+
* @throws IllegalArgumentException if duplicate variable names are found
24+
*/
25+
List<String> getVariableNames();
26+
27+
/**
28+
* Extract URI variable values from the actual request URI.
29+
* <p>
30+
* This method converts the URI template into a regex pattern, then uses that pattern
31+
* to extract variable values from the request URI.
32+
* @param uri The actual URI from the request
33+
* @return A map of variable names to their values
34+
* @throws IllegalArgumentException if the URI template is invalid or the request URI
35+
* doesn't match the template pattern
36+
*/
37+
Map<String, String> extractVariableValues(String uri);
38+
39+
/**
40+
* Indicate whether the given URI matches this template.
41+
* @param uri the URI to match to
42+
* @return {@code true} if it matches; {@code false} otherwise
43+
*/
44+
boolean matches(String uri);
45+
46+
/**
47+
* Check if the given URI is a URI template.
48+
* @return Returns true if the URI contains variables in the format {variableName}
49+
*/
50+
public boolean isUriTemplate(String uri);
51+
52+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2025 - 2025 the original author or authors.
3+
*/
4+
package io.modelcontextprotocol.util;
5+
6+
/**
7+
* Factory interface for creating instances of {@link McpUriTemplateManager}.
8+
*
9+
* @author Christian Tzolov
10+
*/
11+
public interface McpUriTemplateManagerFactory {
12+
13+
/**
14+
* Creates a new instance of {@link McpUriTemplateManager} with the specified URI
15+
* template.
16+
* @param uriTemplate The URI template to be used for variable extraction
17+
* @return A new instance of {@link McpUriTemplateManager}
18+
* @throws IllegalArgumentException if the URI template is null or empty
19+
*/
20+
McpUriTemplateManager create(String uriTemplate);
21+
22+
}

0 commit comments

Comments
 (0)