Skip to content

Adding support for resource template #25

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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/mvnw text eol=lf
*.cmd text eol=crlf

Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public abstract class AbstractMcpSyncServerTests {

private static final String TEST_RESOURCE_URI = "test://resource";

private static final String TEST_RESOURCE_TEMPLATE_URI = "test://resource/{id}";

private static final String TEST_PROMPT_NAME = "test-prompt";

abstract protected McpServerTransportProvider createMcpTransportProvider();
Expand Down Expand Up @@ -208,6 +210,26 @@ void testAddResource() {
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
}

// @Test
// void testAddResourceTemplate() {
// var mcpSyncServer = McpServer.sync(createMcpTransport())
// .serverInfo("test-server", "1.0.0")
// .capabilities(ServerCapabilities.builder().resources(true, false).build())
// .build();
//
// McpSchema.ResourceTemplate resource = new
// McpSchema.ResourceTemplate(TEST_RESOURCE_TEMPLATE_URI,
// "Test Resource", "text/plain", "Test resource description", null);
// McpServerFeatures.SyncResourceTemplateRegistration registration = new
// McpServerFeatures.SyncResourceTemplateRegistration(
// resource, req -> new ReadResourceResult(List.of()));
//
// assertThatCode(() ->
// mcpSyncServer.addResourceTemplate(registration)).doesNotThrowAnyException();
//
// assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException();
// }

@Test
void testAddResourceWithNullSpecifiation() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
Expand All @@ -222,6 +244,23 @@ void testAddResourceWithNullSpecifiation() {
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
}

// @Test
// void testAddResourceTemplateWithNullRegistration() {
// var mcpSyncServer = McpServer.sync(createMcpTransport())
// .serverInfo("test-server", "1.0.0")
// .capabilities(ServerCapabilities.builder().resources(true, false).build())
// .build();
//
// assertThatThrownBy(
// () ->
// mcpSyncServer.addResourceTemplate((McpServerFeatures.SyncResourceTemplateRegistration)
// null))
// .isInstanceOf(McpError.class)
// .hasMessage("Resource must not be null");
//
// assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
// }

@Test
void testAddResourceWithoutCapability() {
var serverWithoutResources = McpServer.sync(createMcpTransportProvider())
Expand All @@ -237,6 +276,23 @@ void testAddResourceWithoutCapability() {
.hasMessage("Server must be configured with resource capabilities");
}

// @Test
// void testAddResourceTemplateWithoutCapability() {
// var serverWithoutResources =
// McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
//
// McpSchema.ResourceTemplate resource = new
// McpSchema.ResourceTemplate(TEST_RESOURCE_TEMPLATE_URI,
// "Test Resource", "text/plain", "Test resource description", null);
// McpServerFeatures.SyncResourceTemplateRegistration registration = new
// McpServerFeatures.SyncResourceTemplateRegistration(
// resource, req -> new ReadResourceResult(List.of()));
//
// assertThatThrownBy(() ->
// serverWithoutResources.addResourceTemplate(registration)).isInstanceOf(McpError.class)
// .hasMessage("Server must be configured with resource capabilities");
// }

@Test
void testRemoveResourceWithoutCapability() {
var serverWithoutResources = McpServer.sync(createMcpTransportProvider())
Expand All @@ -247,6 +303,17 @@ void testRemoveResourceWithoutCapability() {
.hasMessage("Server must be configured with resource capabilities");
}

// @Test
// void testRemoveResourceTemplateWithoutCapability() {
// var serverWithoutResources =
// McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
//
// assertThatThrownBy(() ->
// serverWithoutResources.removeResourceTemplate(TEST_RESOURCE_TEMPLATE_URI))
// .isInstanceOf(McpError.class)
// .hasMessage("Server must be configured with resource capabilities");
// }

// ---------------------------------------
// Prompts Tests
// ---------------------------------------
Expand Down
110 changes: 101 additions & 9 deletions mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.util.UriTemplate;
import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -183,6 +184,27 @@ public Mono<Void> notifyResourcesListChanged() {
return this.delegate.notifyResourcesListChanged();
}

// ---------------------------------------
// Resource Template Management
// ---------------------------------------
/**
* Add a new resource template handler at runtime.
* @param resourceHandler The resource template handler to add
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> addResourceTemplate(McpServerFeatures.AsyncResourceTemplateSpecification resourceHandler) {
return this.delegate.addResourceTemplate(resourceHandler);
}

/**
* Remove a resource template handler at runtime.
* @param resourceUri The URI of the resource template handler to remove
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> removeResourceTemplate(String resourceUri) {
return this.delegate.removeResourceTemplate(resourceUri);
}

// ---------------------------------------
// Prompt Management
// ---------------------------------------
Expand Down Expand Up @@ -258,7 +280,7 @@ private static class AsyncServerImpl extends McpAsyncServer {

private final CopyOnWriteArrayList<McpServerFeatures.AsyncToolSpecification> tools = new CopyOnWriteArrayList<>();

private final CopyOnWriteArrayList<McpSchema.ResourceTemplate> resourceTemplates = new CopyOnWriteArrayList<>();
private final ConcurrentHashMap<UriTemplate, McpServerFeatures.AsyncResourceTemplateSpecification> resourceTemplates = new ConcurrentHashMap<>();

private final ConcurrentHashMap<String, McpServerFeatures.AsyncResourceSpecification> resources = new ConcurrentHashMap<>();

Expand All @@ -279,7 +301,7 @@ private static class AsyncServerImpl extends McpAsyncServer {
this.instructions = features.instructions();
this.tools.addAll(features.tools());
this.resources.putAll(features.resources());
this.resourceTemplates.addAll(features.resourceTemplates());
this.resourceTemplates.putAll(features.resourceTemplates());
this.prompts.putAll(features.prompts());

Map<String, McpServerSession.RequestHandler<?>> requestHandlers = new HashMap<>();
Expand Down Expand Up @@ -488,6 +510,72 @@ private McpServerSession.RequestHandler<CallToolResult> toolsCallRequestHandler(
};
}

// ---------------------------------------
// Resource Template Management
// ---------------------------------------

@Override
public Mono<Void> addResourceTemplate(
McpServerFeatures.AsyncResourceTemplateSpecification resourceTemplateSpecification) {
if (resourceTemplateSpecification == null || resourceTemplateSpecification.resource() == null) {
return Mono.error(new McpError("Resource template must not be null"));
}

if (this.serverCapabilities.resources() == null) {
return Mono.error(new McpError("Server must be configured with resource capabilities"));
}

return Mono.defer(() -> {
if (this.resourceTemplates.putIfAbsent(
new UriTemplate(resourceTemplateSpecification.resource().uriTemplate()),
resourceTemplateSpecification) != null) {
return Mono.error(new McpError("Resource template with URI Template'"
+ resourceTemplateSpecification.resource().uriTemplate() + "' already exists"));
}
logger.debug("Added resource template handler: {}",
resourceTemplateSpecification.resource().uriTemplate());
if (this.serverCapabilities.resources().listChanged()) {
return notifyResourcesListChanged();
}
return Mono.empty();
});
}

@Override
public Mono<Void> removeResourceTemplate(String resourceUriTemplate) {
if (resourceUriTemplate == null) {
return Mono.error(new McpError("Resource Template URI must not be null"));
}
if (this.serverCapabilities.resources() == null) {
return Mono.error(new McpError("Server must be configured with resource capabilities"));
}

return Mono.defer(() -> {
// Lookup the key in the map using the UriTemplate
McpServerFeatures.AsyncResourceTemplateSpecification removed = this.resourceTemplates
.remove(new UriTemplate(resourceUriTemplate));
if (removed != null) {
logger.debug("Removed resource template handler: {}", resourceUriTemplate);
if (this.serverCapabilities.resources().listChanged()) {
return notifyResourcesListChanged();
}
return Mono.empty();
}
return Mono
.error(new McpError("Resource template with URI template '" + resourceUriTemplate + "' not found"));
});
}

private McpServerSession.RequestHandler<McpSchema.ListResourceTemplatesResult> resourceTemplateListRequestHandler() {
return (exchange, params) -> {
var resourceList = this.resourceTemplates.values()
.stream()
.map(McpServerFeatures.AsyncResourceTemplateSpecification::resource)
.toList();
return Mono.just(new McpSchema.ListResourceTemplatesResult(resourceList, null));
};
}

// ---------------------------------------
// Resource Management
// ---------------------------------------
Expand Down Expand Up @@ -552,22 +640,26 @@ private McpServerSession.RequestHandler<McpSchema.ListResourcesResult> resources
};
}

private McpServerSession.RequestHandler<McpSchema.ListResourceTemplatesResult> resourceTemplateListRequestHandler() {
return (exchange, params) -> Mono
.just(new McpSchema.ListResourceTemplatesResult(this.resourceTemplates, null));

}

private McpServerSession.RequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
return (exchange, params) -> {
McpSchema.ReadResourceRequest resourceRequest = objectMapper.convertValue(params,
new TypeReference<McpSchema.ReadResourceRequest>() {
new TypeReference<>() {
});
var resourceUri = resourceRequest.uri();
McpServerFeatures.AsyncResourceSpecification specification = this.resources.get(resourceUri);
if (specification != null) {
return specification.readHandler().apply(exchange, resourceRequest);
}

// If the resource is not found, we can check if it is a template
for (var entry : this.resourceTemplates.entrySet()) {
UriTemplate resourceUriTemplate = entry.getKey();
if (resourceUriTemplate.matchesTemplate(resourceUri)) {
McpServerFeatures.AsyncResourceTemplateSpecification spec = entry.getValue();
return spec.readHandler().apply(exchange, resourceRequest);
}
}

return Mono.error(new McpError("Resource not found: " + resourceUri));
};
}
Expand Down
56 changes: 36 additions & 20 deletions mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpSchema;
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.UriTemplate;
import reactor.core.publisher.Mono;

/**
Expand Down Expand Up @@ -173,14 +173,21 @@ class AsyncSpecification {

/**
* The Model Context Protocol (MCP) provides a standardized way for servers to
* expose resources to clients. Resources allow servers to share data that
* expose parameterized resources using URI templates. Resources allow servers to
* share data that provides context to language models, such as files, database
* schemas, or application-specific information. Each resource is uniquely
* identified by a URI.
*/
private final Map<String, McpServerFeatures.AsyncResourceSpecification> resources = new HashMap<>();

/**
* The Model Context Protocol (MCP) provides a standardized way for servers to
* expose resource template to clients. Resources allow servers to share data that
* provides context to language models, such as files, database schemas, or
* application-specific information. Each resource is uniquely identified by a
* URI.
*/
private final Map<String, McpServerFeatures.AsyncResourceSpecification> resources = new HashMap<>();

private final List<ResourceTemplate> resourceTemplates = new ArrayList<>();
private final Map<UriTemplate, McpServerFeatures.AsyncResourceTemplateSpecification> resourceTemplates = new HashMap<>();

/**
* The Model Context Protocol (MCP) provides a standardized way for servers to
Expand Down Expand Up @@ -409,26 +416,31 @@ public AsyncSpecification resources(McpServerFeatures.AsyncResourceSpecification
* templates.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if resourceTemplates is null.
* @see #resourceTemplates(ResourceTemplate...)
* @see #resourceTemplates(McpServerFeatures.AsyncResourceTemplateSpecification...)
*/
public AsyncSpecification resourceTemplates(List<ResourceTemplate> resourceTemplates) {
public AsyncSpecification resourceTemplates(
List<McpServerFeatures.AsyncResourceTemplateSpecification> resourceTemplates) {
Assert.notNull(resourceTemplates, "Resource templates must not be null");
this.resourceTemplates.addAll(resourceTemplates);
for (McpServerFeatures.AsyncResourceTemplateSpecification resource : resourceTemplates) {
this.resourceTemplates.put(new UriTemplate(resource.resource().uriTemplate()), resource);
}
return this;
}

/**
* Sets the resource templates using varargs for convenience. This is an
* alternative to {@link #resourceTemplates(List)}.
* @param resourceTemplates The resource templates to set.
* @param resourceTemplatesSpecifications The resource templates to set.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if resourceTemplates is null.
* @see #resourceTemplates(List)
*/
public AsyncSpecification resourceTemplates(ResourceTemplate... resourceTemplates) {
Assert.notNull(resourceTemplates, "Resource templates must not be null");
for (ResourceTemplate resourceTemplate : resourceTemplates) {
this.resourceTemplates.add(resourceTemplate);
public AsyncSpecification resourceTemplates(
McpServerFeatures.AsyncResourceTemplateSpecification... resourceTemplatesSpecifications) {
Assert.notNull(resourceTemplatesSpecifications, "Resource templates must not be null");
for (McpServerFeatures.AsyncResourceTemplateSpecification resourceTemplate : resourceTemplatesSpecifications) {
this.resourceTemplates.put(new UriTemplate(resourceTemplate.resource().uriTemplate()),
resourceTemplate);
}
return this;
}
Expand Down Expand Up @@ -606,7 +618,7 @@ class SyncSpecification {
*/
private final Map<String, McpServerFeatures.SyncResourceSpecification> resources = new HashMap<>();

private final List<ResourceTemplate> resourceTemplates = new ArrayList<>();
private final Map<String, McpServerFeatures.SyncResourceTemplateSpecification> resourceTemplates = new HashMap<>();

/**
* The Model Context Protocol (MCP) provides a standardized way for servers to
Expand Down Expand Up @@ -834,11 +846,14 @@ public SyncSpecification resources(McpServerFeatures.SyncResourceSpecification..
* templates.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if resourceTemplates is null.
* @see #resourceTemplates(ResourceTemplate...)
* @see #resourceTemplates(McpServerFeatures.SyncResourceTemplateSpecification...)
*/
public SyncSpecification resourceTemplates(List<ResourceTemplate> resourceTemplates) {
public SyncSpecification resourceTemplates(
List<McpServerFeatures.SyncResourceTemplateSpecification> resourceTemplates) {
Assert.notNull(resourceTemplates, "Resource templates must not be null");
this.resourceTemplates.addAll(resourceTemplates);
for (McpServerFeatures.SyncResourceTemplateSpecification resource : resourceTemplates) {
this.resourceTemplates.put(resource.resource().uriTemplate(), resource);
}
return this;
}

Expand All @@ -850,10 +865,11 @@ public SyncSpecification resourceTemplates(List<ResourceTemplate> resourceTempla
* @throws IllegalArgumentException if resourceTemplates is null
* @see #resourceTemplates(List)
*/
public SyncSpecification resourceTemplates(ResourceTemplate... resourceTemplates) {
public SyncSpecification resourceTemplates(
McpServerFeatures.SyncResourceTemplateSpecification... resourceTemplates) {
Assert.notNull(resourceTemplates, "Resource templates must not be null");
for (ResourceTemplate resourceTemplate : resourceTemplates) {
this.resourceTemplates.add(resourceTemplate);
for (McpServerFeatures.SyncResourceTemplateSpecification resource : resourceTemplates) {
this.resourceTemplates.put(resource.resource().uriTemplate(), resource);
}
return this;
}
Expand Down
Loading