Skip to content

feat: Add URI template support for MCP resources #208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,8 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) {
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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,10 +23,13 @@
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;
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -274,8 +280,11 @@ private static class AsyncServerImpl extends 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();
Expand All @@ -286,6 +295,7 @@ private static class AsyncServerImpl extends 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<>();

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

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() {
Expand All @@ -574,11 +602,16 @@ private McpServerSession.RequestHandler<McpSchema.ReadResourceResult> resourcesR
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);
};
}

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

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());
Expand Down
68 changes: 66 additions & 2 deletions mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
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;

/**
Expand Down Expand Up @@ -156,6 +158,8 @@ class AsyncSpecification {

private final McpServerTransportProvider transportProvider;

private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();

private ObjectMapper objectMapper;

private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
Expand Down Expand Up @@ -204,6 +208,19 @@ private AsyncSpecification(McpServerTransportProvider 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
* timeout applies to all requests made through the client, including tool calls,
Expand Down Expand Up @@ -517,6 +534,36 @@ public AsyncSpecification prompts(McpServerFeatures.AsyncPromptSpecification...
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
Expand Down Expand Up @@ -587,7 +634,8 @@ public McpAsyncServer build() {
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);
}

}
Expand All @@ -600,6 +648,8 @@ class SyncSpecification {
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;
Expand Down Expand Up @@ -650,6 +700,19 @@ private SyncSpecification(McpServerTransportProvider 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
* timeout applies to all requests made through the client, including tool calls,
Expand Down Expand Up @@ -1064,7 +1127,8 @@ public McpSyncServer build() {
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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}
Loading