Skip to content

Commit e610d85

Browse files
committed
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 <[email protected]>
1 parent 261554b commit e610d85

File tree

8 files changed

+489
-16
lines changed

8 files changed

+489
-16
lines changed

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

+2-1
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

+64-13
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;
@@ -92,8 +96,10 @@ public class McpAsyncServer {
9296
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
9397
*/
9498
McpAsyncServer(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
95-
McpServerFeatures.Async features, Duration requestTimeout) {
96-
this.delegate = new AsyncServerImpl(mcpTransportProvider, objectMapper, requestTimeout, features);
99+
McpServerFeatures.Async features, Duration requestTimeout,
100+
McpUriTemplateManagerFactory uriTemplateManagerFactory) {
101+
this.delegate = new AsyncServerImpl(mcpTransportProvider, objectMapper, requestTimeout, features,
102+
uriTemplateManagerFactory);
97103
}
98104

99105
/**
@@ -274,8 +280,11 @@ private static class AsyncServerImpl extends McpAsyncServer {
274280

275281
private List<String> protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
276282

283+
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
284+
277285
AsyncServerImpl(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
278-
Duration requestTimeout, McpServerFeatures.Async features) {
286+
Duration requestTimeout, McpServerFeatures.Async features,
287+
McpUriTemplateManagerFactory uriTemplateManagerFactory) {
279288
this.mcpTransportProvider = mcpTransportProvider;
280289
this.objectMapper = objectMapper;
281290
this.serverInfo = features.serverInfo();
@@ -286,6 +295,7 @@ private static class AsyncServerImpl extends McpAsyncServer {
286295
this.resourceTemplates.addAll(features.resourceTemplates());
287296
this.prompts.putAll(features.prompts());
288297
this.completions.putAll(features.completions());
298+
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
289299

290300
Map<String, McpServerSession.RequestHandler<?>> requestHandlers = new HashMap<>();
291301

@@ -564,8 +574,26 @@ private McpServerSession.RequestHandler<McpSchema.ListResourcesResult> resources
564574

565575
private McpServerSession.RequestHandler<McpSchema.ListResourceTemplatesResult> resourceTemplateListRequestHandler() {
566576
return (exchange, params) -> Mono
567-
.just(new McpSchema.ListResourceTemplatesResult(this.resourceTemplates, null));
577+
.just(new McpSchema.ListResourceTemplatesResult(this.getResourceTemplates(), null));
578+
579+
}
568580

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

571599
private McpServerSession.RequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
@@ -574,11 +602,16 @@ private McpServerSession.RequestHandler<McpSchema.ReadResourceResult> resourcesR
574602
new TypeReference<McpSchema.ReadResourceRequest>() {
575603
});
576604
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));
605+
606+
McpServerFeatures.AsyncResourceSpecification specification = this.resources.values()
607+
.stream()
608+
.filter(resourceSpecification -> this.uriTemplateManagerFactory
609+
.create(resourceSpecification.resource().uri())
610+
.matches(resourceUri))
611+
.findFirst()
612+
.orElseThrow(() -> new McpError("Resource not found: " + resourceUri));
613+
614+
return specification.readHandler().apply(exchange, resourceRequest);
582615
};
583616
}
584617

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

730763
String type = request.ref().type();
731764

765+
String argumentName = request.argument().name();
766+
732767
// check if the referenced resource exists
733768
if (type.equals("ref/prompt") && request.ref() instanceof McpSchema.PromptReference promptReference) {
734-
McpServerFeatures.AsyncPromptSpecification prompt = this.prompts.get(promptReference.name());
735-
if (prompt == null) {
769+
McpServerFeatures.AsyncPromptSpecification promptSpec = this.prompts.get(promptReference.name());
770+
if (promptSpec == null) {
736771
return Mono.error(new McpError("Prompt not found: " + promptReference.name()));
737772
}
773+
if (!promptSpec.prompt()
774+
.arguments()
775+
.stream()
776+
.filter(arg -> arg.name().equals(argumentName))
777+
.findFirst()
778+
.isPresent()) {
779+
780+
return Mono.error(new McpError("Argument not found: " + argumentName));
781+
}
738782
}
739783

740784
if (type.equals("ref/resource")
741785
&& request.ref() instanceof McpSchema.ResourceReference resourceReference) {
742-
McpServerFeatures.AsyncResourceSpecification resource = this.resources.get(resourceReference.uri());
743-
if (resource == null) {
786+
McpServerFeatures.AsyncResourceSpecification resourceSpec = this.resources
787+
.get(resourceReference.uri());
788+
if (resourceSpec == null) {
744789
return Mono.error(new McpError("Resource not found: " + resourceReference.uri()));
745790
}
791+
if (!uriTemplateManagerFactory.create(resourceSpec.resource().uri())
792+
.getVariableNames()
793+
.contains(argumentName)) {
794+
return Mono.error(new McpError("Argument not found: " + argumentName));
795+
}
796+
746797
}
747798

748799
McpServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());

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

+66-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
2020
import io.modelcontextprotocol.spec.McpServerTransportProvider;
2121
import io.modelcontextprotocol.util.Assert;
22+
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
23+
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
2224
import reactor.core.publisher.Mono;
2325

2426
/**
@@ -156,6 +158,8 @@ class AsyncSpecification {
156158

157159
private final McpServerTransportProvider transportProvider;
158160

161+
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
162+
159163
private ObjectMapper objectMapper;
160164

161165
private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
@@ -204,6 +208,19 @@ private AsyncSpecification(McpServerTransportProvider transportProvider) {
204208
this.transportProvider = transportProvider;
205209
}
206210

211+
/**
212+
* Sets the URI template manager factory to use for creating URI templates. This
213+
* allows for custom URI template parsing and variable extraction.
214+
* @param uriTemplateManagerFactory The factory to use. Must not be null.
215+
* @return This builder instance for method chaining
216+
* @throws IllegalArgumentException if uriTemplateManagerFactory is null
217+
*/
218+
public AsyncSpecification uriTemplateManagerFactory(McpUriTemplateManagerFactory uriTemplateManagerFactory) {
219+
Assert.notNull(uriTemplateManagerFactory, "URI template manager factory must not be null");
220+
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
221+
return this;
222+
}
223+
207224
/**
208225
* Sets the duration to wait for server responses before timing out requests. This
209226
* timeout applies to all requests made through the client, including tool calls,
@@ -517,6 +534,36 @@ public AsyncSpecification prompts(McpServerFeatures.AsyncPromptSpecification...
517534
return this;
518535
}
519536

537+
/**
538+
* Registers multiple completions with their handlers using a List. This method is
539+
* useful when completions need to be added in bulk from a collection.
540+
* @param completions List of completion specifications. Must not be null.
541+
* @return This builder instance for method chaining
542+
* @throws IllegalArgumentException if completions is null
543+
*/
544+
public AsyncSpecification completions(List<McpServerFeatures.AsyncCompletionSpecification> completions) {
545+
Assert.notNull(completions, "Completions list must not be null");
546+
for (McpServerFeatures.AsyncCompletionSpecification completion : completions) {
547+
this.completions.put(completion.referenceKey(), completion);
548+
}
549+
return this;
550+
}
551+
552+
/**
553+
* Registers multiple completions with their handlers using varargs. This method
554+
* is useful when completions are defined inline and added directly.
555+
* @param completions Array of completion specifications. Must not be null.
556+
* @return This builder instance for method chaining
557+
* @throws IllegalArgumentException if completions is null
558+
*/
559+
public AsyncSpecification completions(McpServerFeatures.AsyncCompletionSpecification... completions) {
560+
Assert.notNull(completions, "Completions list must not be null");
561+
for (McpServerFeatures.AsyncCompletionSpecification completion : completions) {
562+
this.completions.put(completion.referenceKey(), completion);
563+
}
564+
return this;
565+
}
566+
520567
/**
521568
* Registers a consumer that will be notified when the list of roots changes. This
522569
* is useful for updating resource availability dynamically, such as when new
@@ -587,7 +634,8 @@ public McpAsyncServer build() {
587634
this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers,
588635
this.instructions);
589636
var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
590-
return new McpAsyncServer(this.transportProvider, mapper, features, this.requestTimeout);
637+
return new McpAsyncServer(this.transportProvider, mapper, features, this.requestTimeout,
638+
this.uriTemplateManagerFactory);
591639
}
592640

593641
}
@@ -600,6 +648,8 @@ class SyncSpecification {
600648
private static final McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server",
601649
"1.0.0");
602650

651+
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
652+
603653
private final McpServerTransportProvider transportProvider;
604654

605655
private ObjectMapper objectMapper;
@@ -650,6 +700,19 @@ private SyncSpecification(McpServerTransportProvider transportProvider) {
650700
this.transportProvider = transportProvider;
651701
}
652702

703+
/**
704+
* Sets the URI template manager factory to use for creating URI templates. This
705+
* allows for custom URI template parsing and variable extraction.
706+
* @param uriTemplateManagerFactory The factory to use. Must not be null.
707+
* @return This builder instance for method chaining
708+
* @throws IllegalArgumentException if uriTemplateManagerFactory is null
709+
*/
710+
public SyncSpecification uriTemplateManagerFactory(McpUriTemplateManagerFactory uriTemplateManagerFactory) {
711+
Assert.notNull(uriTemplateManagerFactory, "URI template manager factory must not be null");
712+
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
713+
return this;
714+
}
715+
653716
/**
654717
* Sets the duration to wait for server responses before timing out requests. This
655718
* timeout applies to all requests made through the client, including tool calls,
@@ -1064,7 +1127,8 @@ public McpSyncServer build() {
10641127
this.rootsChangeHandlers, this.instructions);
10651128
McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures);
10661129
var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
1067-
var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures, this.requestTimeout);
1130+
var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures, this.requestTimeout,
1131+
this.uriTemplateManagerFactory);
10681132

10691133
return new McpSyncServer(asyncServer);
10701134
}
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+
}

0 commit comments

Comments
 (0)