Skip to content
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 @@ -818,13 +818,13 @@ private NotificationHandler asyncToolsChangeNotificationHandler(
* @see #readResource(McpSchema.Resource)
*/
public Mono<McpSchema.ListResourcesResult> listResources() {
return this.listResources(McpSchema.FIRST_PAGE)
.expand(result -> (result.nextCursor() != null) ? this.listResources(result.nextCursor()) : Mono.empty())
.reduce(new ArrayList<McpSchema.Resource>(), (accumulated, result) -> {
accumulated.addAll(result.resources());
return accumulated;
})
.map(all -> McpSchema.ListResourcesResult.builder(Collections.unmodifiableList(all)).build());
return this.listResources(McpSchema.FIRST_PAGE).expand(result -> {
String next = result.nextCursor();
return (next != null && !next.isEmpty()) ? this.listResources(next) : Mono.empty();
}).reduce(new ArrayList<McpSchema.Resource>(), (accumulated, result) -> {
accumulated.addAll(result.resources());
return accumulated;
}).map(all -> McpSchema.ListResourcesResult.builder(Collections.unmodifiableList(all)).build());
}

/**
Expand Down Expand Up @@ -904,14 +904,13 @@ public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.ReadResourceReq
* @see McpSchema.ListResourceTemplatesResult
*/
public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates() {
return this.listResourceTemplates(McpSchema.FIRST_PAGE)
.expand(result -> (result.nextCursor() != null) ? this.listResourceTemplates(result.nextCursor())
: Mono.empty())
.reduce(new ArrayList<McpSchema.ResourceTemplate>(), (accumulated, result) -> {
accumulated.addAll(result.resourceTemplates());
return accumulated;
})
.map(all -> McpSchema.ListResourceTemplatesResult.builder(Collections.unmodifiableList(all)).build());
return this.listResourceTemplates(McpSchema.FIRST_PAGE).expand(result -> {
String next = result.nextCursor();
return (next != null && !next.isEmpty()) ? this.listResourceTemplates(next) : Mono.empty();
}).reduce(new ArrayList<McpSchema.ResourceTemplate>(), (accumulated, result) -> {
accumulated.addAll(result.resourceTemplates());
return accumulated;
}).map(all -> McpSchema.ListResourceTemplatesResult.builder(Collections.unmodifiableList(all)).build());
}

/**
Expand Down Expand Up @@ -1024,13 +1023,13 @@ private NotificationHandler asyncResourcesUpdatedNotificationHandler(
* @see #getPrompt(GetPromptRequest)
*/
public Mono<ListPromptsResult> listPrompts() {
return this.listPrompts(McpSchema.FIRST_PAGE)
.expand(result -> (result.nextCursor() != null) ? this.listPrompts(result.nextCursor()) : Mono.empty())
.reduce(new ArrayList<McpSchema.Prompt>(), (accumulated, result) -> {
accumulated.addAll(result.prompts());
return accumulated;
})
.map(all -> McpSchema.ListPromptsResult.builder(Collections.unmodifiableList(all)).build());
return this.listPrompts(McpSchema.FIRST_PAGE).expand(result -> {
String next = result.nextCursor();
return (next != null && !next.isEmpty()) ? this.listPrompts(next) : Mono.empty();
}).reduce(new ArrayList<McpSchema.Prompt>(), (accumulated, result) -> {
accumulated.addAll(result.prompts());
return accumulated;
}).map(all -> McpSchema.ListPromptsResult.builder(Collections.unmodifiableList(all)).build());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class DefaultMcpTransportContext implements McpTransportContext {

DefaultMcpTransportContext(Map<String, Object> metadata) {
Assert.notNull(metadata, "The metadata cannot be null");
this.metadata = metadata;
this.metadata = Map.copyOf(metadata);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2026-2026 the original author or authors.
*/

package io.modelcontextprotocol.common;

import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for {@link McpTransportContext#create(Map)}, which is documented to return an
* unmodifiable context.
*/
class McpTransportContextTests {

@Test
void createdContextShouldNotSeeLaterWritesToTheSourceMap() {
Map<String, Object> metadata = new HashMap<>();
metadata.put("tenant", "acme");

McpTransportContext context = McpTransportContext.create(metadata);
metadata.put("tenant", "other");

assertThat(context.get("tenant")).isEqualTo("acme");
}

@Test
void createdContextShouldNotSeeLaterAdditionsToTheSourceMap() {
Map<String, Object> metadata = new HashMap<>();
metadata.put("tenant", "acme");

McpTransportContext context = McpTransportContext.create(metadata);
metadata.put("added-after-the-fact", "surprise");

assertThat(context.get("added-after-the-fact")).isNull();
}

@Test
void createdContextShouldNotBeEmptiedByClearingTheSourceMap() {
Map<String, Object> metadata = new HashMap<>();
metadata.put("tenant", "acme");

McpTransportContext context = McpTransportContext.create(metadata);
metadata.clear();

assertThat(context.get("tenant")).isEqualTo("acme");
}

@Test
void createdContextShouldRemainUsableAsAMapKey() {
Map<String, Object> metadata = new HashMap<>();
metadata.put("tenant", "acme");
McpTransportContext context = McpTransportContext.create(metadata);

Map<McpTransportContext, String> byContext = new HashMap<>();
byContext.put(context, "value");
metadata.put("tenant", "other");

assertThat(byContext.get(context)).isEqualTo("value");
}

@Test
void twoContextsCreatedFromEqualMapsShouldStayEqual() {
Map<String, Object> first = new HashMap<>();
first.put("tenant", "acme");
Map<String, Object> second = new HashMap<>();
second.put("tenant", "acme");

McpTransportContext firstContext = McpTransportContext.create(first);
McpTransportContext secondContext = McpTransportContext.create(second);
assertThat(firstContext).isEqualTo(secondContext);

first.put("tenant", "other");

assertThat(firstContext).isEqualTo(secondContext);
}

@Test
void createdContextFromAnImmutableMapIsAlreadyCorrect() {
McpTransportContext context = McpTransportContext.create(Map.of("tenant", "acme"));

assertThat(context.get("tenant")).isEqualTo("acme");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -298,6 +299,42 @@ void testListPromptsWithCursorAndMeta() {

}

@Test
void listResourcesStopsOnEmptyNextCursor() {
var transport = new EmptyCursorTestMcpClientTransport(McpSchema.METHOD_RESOURCES_LIST);
McpAsyncClient client = McpClient.async(transport).build();

McpSchema.ListResourcesResult result = client.listResources().block();

assertThat(result).isNotNull();
assertThat(result.resources()).extracting(McpSchema.Resource::name).containsExactly("test.txt");
assertThat(transport.getRequestCount()).isEqualTo(1);
}

@Test
void listResourceTemplatesStopsOnEmptyNextCursor() {
var transport = new EmptyCursorTestMcpClientTransport(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST);
McpAsyncClient client = McpClient.async(transport).build();

McpSchema.ListResourceTemplatesResult result = client.listResourceTemplates().block();

assertThat(result).isNotNull();
assertThat(result.resourceTemplates()).extracting(McpSchema.ResourceTemplate::name).containsExactly("template");
assertThat(transport.getRequestCount()).isEqualTo(1);
}

@Test
void listPromptsStopsOnEmptyNextCursor() {
var transport = new EmptyCursorTestMcpClientTransport(McpSchema.METHOD_PROMPT_LIST);
McpAsyncClient client = McpClient.async(transport).build();

McpSchema.ListPromptsResult result = client.listPrompts().block();

assertThat(result).isNotNull();
assertThat(result.prompts()).extracting(McpSchema.Prompt::name).containsExactly("test-prompt");
assertThat(transport.getRequestCount()).isEqualTo(1);
}

static class TestMcpClientTransport implements McpClientTransport {

private Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler;
Expand Down Expand Up @@ -397,4 +434,90 @@ public McpSchema.PaginatedRequest getCapturedRequest() {

}

static class EmptyCursorTestMcpClientTransport implements McpClientTransport {

private final String listMethod;

private final AtomicInteger requestCount = new AtomicInteger();

private Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler;

EmptyCursorTestMcpClientTransport(String listMethod) {
this.listMethod = listMethod;
}

@Override
public Mono<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler) {
this.handler = handler;
return Mono.empty();
}

@Override
public Mono<Void> closeGracefully() {
return Mono.empty();
}

@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
if (!(message instanceof McpSchema.JSONRPCRequest request)) {
return Mono.empty();
}

McpSchema.JSONRPCResponse response;
if (McpSchema.METHOD_INITIALIZE.equals(request.method())) {
McpSchema.ServerCapabilities caps = McpSchema.ServerCapabilities.builder()
.prompts(false)
.resources(false, false)
.tools(false)
.build();

McpSchema.InitializeResult initResult = McpSchema.InitializeResult
.builder(ProtocolVersions.MCP_2024_11_05, caps, MOCK_SERVER_INFO)
.build();
response = McpSchema.JSONRPCResponse.result(request.id(), initResult);
}
else if (this.listMethod.equals(request.method())) {
this.requestCount.incrementAndGet();
response = McpSchema.JSONRPCResponse.result(request.id(), resultForMethod(request.method()));
}
else {
return Mono.empty();
}

return this.handler.apply(Mono.just(response)).then();
}

private Object resultForMethod(String method) {
if (McpSchema.METHOD_RESOURCES_LIST.equals(method)) {
McpSchema.Resource resource = McpSchema.Resource.builder("file:///test.txt", "test.txt").build();
return McpSchema.ListResourcesResult.builder(List.of(resource)).nextCursor("").build();
}
if (McpSchema.METHOD_RESOURCES_TEMPLATES_LIST.equals(method)) {
McpSchema.ResourceTemplate template = McpSchema.ResourceTemplate.builder("file:///{name}", "template")
.build();
return McpSchema.ListResourceTemplatesResult.builder(List.of(template)).nextCursor("").build();
}
if (McpSchema.METHOD_PROMPT_LIST.equals(method)) {
McpSchema.Prompt prompt = McpSchema.Prompt.builder("test-prompt").build();
return McpSchema.ListPromptsResult.builder(List.of(prompt)).nextCursor("").build();
}
throw new IllegalArgumentException("Unsupported method: " + method);
}

@Override
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
return JSON_MAPPER.convertValue(data, new TypeRef<>() {
@Override
public java.lang.reflect.Type getType() {
return typeRef.getType();
}
});
}

int getRequestCount() {
return this.requestCount.get();
}

}

}
Loading