diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientListChangedAnnotationsScanningIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientListChangedAnnotationsScanningIT.java index e4784bbdac..cc67e5c064 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientListChangedAnnotationsScanningIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientListChangedAnnotationsScanningIT.java @@ -69,8 +69,8 @@ public void shouldScanAllThreeListChangedAnnotationsSync() { new McpSchema.Prompt("prompt-1", "a test prompt", Collections.emptyList()), new McpSchema.Prompt("prompt-2", "another test prompt", Collections.emptyList())); List updatedResources = List.of( - McpSchema.Resource.builder().name("resource-1").uri("file:///resource/1").build(), - McpSchema.Resource.builder().name("resource-2").uri("file:///resource/2").build()); + McpSchema.Resource.builder("resource-1", "file:///resource/1").build(), + McpSchema.Resource.builder("resource-2", "file:///resource/2").build()); registry.handleToolListChanged("test-client", updatedTools); registry.handleResourceListChanged("test-client", updatedResources); @@ -100,8 +100,8 @@ public void shouldScanAllThreeListChangedAnnotationsAsync() { new McpSchema.Prompt("prompt-1", "a test prompt", Collections.emptyList()), new McpSchema.Prompt("prompt-2", "another test prompt", Collections.emptyList())); List updatedResources = List.of( - McpSchema.Resource.builder().name("resource-1").uri("file:///resource/1").build(), - McpSchema.Resource.builder().name("resource-2").uri("file:///resource/2").build()); + McpSchema.Resource.builder("resource-1", "file:///resource/1").build(), + McpSchema.Resource.builder("resource-2", "file:///resource/2").build()); registry.handleToolListChanged("test-client", updatedTools).block(); registry.handleResourceListChanged("test-client", updatedResources).block(); diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/SseWebClientWebFluxServerIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/SseWebClientWebFluxServerIT.java index 3382b58995..49e1d70884 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/SseWebClientWebFluxServerIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/SseWebClientWebFluxServerIT.java @@ -258,12 +258,11 @@ void clientServerCapabilities() { assertThat(mcpClient.listResources()).isNotNull(); assertThat(mcpClient.listResources().resources()).hasSize(1); assertThat(mcpClient.listResources().resources().get(0)) - .isEqualToComparingFieldByFieldRecursively(Resource.builder() - .uri("file://resource") - .name("Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .build()); + .isEqualToComparingFieldByFieldRecursively( + Resource.builder("file://resource", "Test Resource") + .mimeType("text/plain") + .description("Test resource description") + .build()); }); @@ -435,9 +434,7 @@ public List myCompletions() { @Bean public List myResources() { - var systemInfoResource = Resource.builder() - .uri("file://resource") - .name("Test Resource") + var systemInfoResource = Resource.builder("file://resource", "Test Resource") .mimeType("text/plain") .description("Test resource description") .build(); diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StatelessWebClientWebFluxServerIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StatelessWebClientWebFluxServerIT.java index 45163f7629..d1abbb0e04 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StatelessWebClientWebFluxServerIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StatelessWebClientWebFluxServerIT.java @@ -220,12 +220,11 @@ void clientServerCapabilities() { assertThat(mcpClient.listResources()).isNotNull(); assertThat(mcpClient.listResources().resources()).hasSize(1); assertThat(mcpClient.listResources().resources().get(0)) - .isEqualToComparingFieldByFieldRecursively(Resource.builder() - .uri("file://resource") - .name("Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .build()); + .isEqualToComparingFieldByFieldRecursively( + Resource.builder("file://resource", "Test Resource") + .mimeType("text/plain") + .description("Test resource description") + .build()); }); @@ -352,9 +351,7 @@ public List myCompletion @Bean public List myResources() { - var systemInfoResource = Resource.builder() - .uri("file://resource") - .name("Test Resource") + var systemInfoResource = Resource.builder("file://resource", "Test Resource") .mimeType("text/plain") .description("Test resource description") .build(); diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotations2IT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotations2IT.java index 3afc5b5ddb..b793c7e916 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotations2IT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotations2IT.java @@ -238,12 +238,11 @@ void clientServerCapabilities() { assertThat(mcpClient.listResources()).isNotNull(); assertThat(mcpClient.listResources().resources()).hasSize(1); assertThat(mcpClient.listResources().resources().get(0)) - .isEqualToComparingFieldByFieldRecursively(Resource.builder() - .uri("file://resource") - .name("Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .build()); + .isEqualToComparingFieldByFieldRecursively( + Resource.builder("file://resource", "Test Resource") + .mimeType("text/plain") + .description("Test resource description") + .build()); // PROMPT / COMPLETION diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsIT.java index 8e2eeb08f2..de46bb1daf 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsIT.java @@ -239,12 +239,11 @@ void clientServerCapabilities() { assertThat(mcpClient.listResources()).isNotNull(); assertThat(mcpClient.listResources().resources()).hasSize(1); assertThat(mcpClient.listResources().resources().get(0)) - .isEqualToComparingFieldByFieldRecursively(Resource.builder() - .uri("file://resource") - .name("Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .build()); + .isEqualToComparingFieldByFieldRecursively( + Resource.builder("file://resource", "Test Resource") + .mimeType("text/plain") + .description("Test resource description") + .build()); // PROMPT / COMPLETION diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsManualIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsManualIT.java index 0c7da11f8b..a359ccfc21 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsManualIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableMcpAnnotationsManualIT.java @@ -246,12 +246,11 @@ void clientServerCapabilities() { assertThat(mcpClient.listResources()).isNotNull(); assertThat(mcpClient.listResources().resources()).hasSize(1); assertThat(mcpClient.listResources().resources().get(0)) - .isEqualToComparingFieldByFieldRecursively(Resource.builder() - .uri("file://resource") - .name("Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .build()); + .isEqualToComparingFieldByFieldRecursively( + Resource.builder("file://resource", "Test Resource") + .mimeType("text/plain") + .description("Test resource description") + .build()); // PROMPT / COMPLETION diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableWebClientWebFluxServerIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableWebClientWebFluxServerIT.java index dd49157367..a06c681aaa 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableWebClientWebFluxServerIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/webflux/autoconfigure/StreamableWebClientWebFluxServerIT.java @@ -266,12 +266,11 @@ void clientServerCapabilities() { assertThat(mcpClient.listResources()).isNotNull(); assertThat(mcpClient.listResources().resources()).hasSize(1); assertThat(mcpClient.listResources().resources().get(0)) - .isEqualToComparingFieldByFieldRecursively(Resource.builder() - .uri("file://resource") - .name("Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .build()); + .isEqualToComparingFieldByFieldRecursively( + Resource.builder("file://resource", "Test Resource") + .mimeType("text/plain") + .description("Test resource description") + .build()); }); @@ -434,9 +433,7 @@ public List myCompletions() { @Bean public List myResources() { - var systemInfoResource = Resource.builder() - .uri("file://resource") - .name("Test Resource") + var systemInfoResource = Resource.builder("file://resource", "Test Resource") .mimeType("text/plain") .description("Test resource description") .build(); diff --git a/mcp/common/src/test/java/org/springframework/ai/mcp/AsyncMcpToolCallbackTest.java b/mcp/common/src/test/java/org/springframework/ai/mcp/AsyncMcpToolCallbackTest.java index 4b23e4b91d..1c276541a7 100644 --- a/mcp/common/src/test/java/org/springframework/ai/mcp/AsyncMcpToolCallbackTest.java +++ b/mcp/common/src/test/java/org/springframework/ai/mcp/AsyncMcpToolCallbackTest.java @@ -202,8 +202,7 @@ void callShouldIncludeToolContext() { @Test void getToolDefinitionShouldReturnCorrectDefinition() { when(this.tool.description()).thenReturn("Test tool description"); - var jsonSchema = mock(Map.class); - when(this.tool.inputSchema()).thenReturn(jsonSchema); + when(this.tool.inputSchema()).thenReturn(Map.of()); // Act var callback = AsyncMcpToolCallback.builder() @@ -294,7 +293,7 @@ void builderShouldAcceptCustomToolContextConverter() { void deprecatedConstructorShouldWork() { when(this.tool.name()).thenReturn("testTool"); when(this.tool.description()).thenReturn("Test description"); - when(this.tool.inputSchema()).thenReturn(mock(Map.class)); + when(this.tool.inputSchema()).thenReturn(Map.of()); var clientInfo = new Implementation("testClient", "1.0.0"); when(this.mcpClient.getClientInfo()).thenReturn(clientInfo); diff --git a/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderBuilderTest.java b/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderBuilderTest.java index ced110d46f..0862e2589e 100644 --- a/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderBuilderTest.java +++ b/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderBuilderTest.java @@ -254,7 +254,7 @@ private McpSyncClient createMockClient(String clientName, String toolName) { Tool tool = Mockito.mock(Tool.class); when(tool.name()).thenReturn(toolName); when(tool.description()).thenReturn("Test tool description"); - when(tool.inputSchema()).thenReturn(Mockito.mock(Map.class)); + when(tool.inputSchema()).thenReturn(Map.of()); // Mock list tools response McpSchema.ListToolsResult listToolsResult = Mockito.mock(McpSchema.ListToolsResult.class); diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/adapter/ResourceAdapter.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/adapter/ResourceAdapter.java index 5bf89b83b5..ebccd782f1 100644 --- a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/adapter/ResourceAdapter.java +++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/adapter/ResourceAdapter.java @@ -46,9 +46,7 @@ public static McpSchema.Resource asResource(McpResource mcpResourceAnnotation) { } var meta = MetaUtils.getMeta(mcpResourceAnnotation.metaProvider()); - var resourceBuilder = McpSchema.Resource.builder() - .uri(mcpResourceAnnotation.uri()) - .name(name) + var resourceBuilder = McpSchema.Resource.builder(mcpResourceAnnotation.uri(), name) .title(mcpResourceAnnotation.title()) .description(mcpResourceAnnotation.description()) .mimeType(mcpResourceAnnotation.mimeType()) @@ -75,9 +73,7 @@ public static McpSchema.ResourceTemplate asResourceTemplate(McpResource mcpResou } var meta = MetaUtils.getMeta(mcpResource.metaProvider()); - return McpSchema.ResourceTemplate.builder() - .uriTemplate(mcpResource.uri()) - .name(name) + return McpSchema.ResourceTemplate.builder(mcpResource.uri(), name) .description(mcpResource.description()) .mimeType(mcpResource.mimeType()) .meta(meta) diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncMcpResourceProvider.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncMcpResourceProvider.java index 715cf130d8..389eadd2f6 100644 --- a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncMcpResourceProvider.java +++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncMcpResourceProvider.java @@ -93,9 +93,7 @@ public List getResourceSpecifications() { var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); - var mcpResource = McpSchema.Resource.builder() - .uri(uri) - .name(name) + var mcpResource = McpSchema.Resource.builder(uri, name) .description(description) .mimeType(mimeType) .meta(meta) @@ -146,9 +144,7 @@ public List getResourceTemplateSpecification var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); - var mcpResourceTemplate = McpSchema.ResourceTemplate.builder() - .uriTemplate(uri) - .name(name) + var mcpResourceTemplate = McpSchema.ResourceTemplate.builder(uri, name) .description(description) .mimeType(mimeType) .meta(meta) diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncStatelessMcpResourceProvider.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncStatelessMcpResourceProvider.java index 1c62984c6f..ffdd7eff5f 100644 --- a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncStatelessMcpResourceProvider.java +++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncStatelessMcpResourceProvider.java @@ -93,9 +93,7 @@ public List getResourceSpecifications() { var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); - var mcpResource = McpSchema.Resource.builder() - .uri(uri) - .name(name) + var mcpResource = McpSchema.Resource.builder(uri, name) .description(description) .mimeType(mimeType) .meta(meta) @@ -146,9 +144,7 @@ public List getResourceTemplateSpecification var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); - var mcpResourceTemplate = McpSchema.ResourceTemplate.builder() - .uriTemplate(uri) - .name(name) + var mcpResourceTemplate = McpSchema.ResourceTemplate.builder(uri, name) .description(description) .mimeType(mimeType) .meta(meta) diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncMcpResourceProvider.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncMcpResourceProvider.java index 04d4b13c27..c152c22ac0 100644 --- a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncMcpResourceProvider.java +++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncMcpResourceProvider.java @@ -67,9 +67,7 @@ public List getResourceSpecifications() { var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); - var mcpResource = McpSchema.Resource.builder() - .uri(uri) - .name(name) + var mcpResource = McpSchema.Resource.builder(uri, name) .description(description) .mimeType(mimeType) .meta(meta) @@ -112,9 +110,7 @@ public List getResourceTemplateSpecifications var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); - var mcpResourceTemplate = McpSchema.ResourceTemplate.builder() - .uriTemplate(uri) - .name(name) + var mcpResourceTemplate = McpSchema.ResourceTemplate.builder(uri, name) .description(description) .mimeType(mimeType) .meta(meta) diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncStatelessMcpResourceProvider.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncStatelessMcpResourceProvider.java index 05b579fa57..d384c35281 100644 --- a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncStatelessMcpResourceProvider.java +++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncStatelessMcpResourceProvider.java @@ -92,9 +92,7 @@ public List getResourceSpecifications() { var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); - var mcpResource = McpSchema.Resource.builder() - .uri(uri) - .name(name) + var mcpResource = McpSchema.Resource.builder(uri, name) .description(description) .mimeType(mimeType) .meta(meta) @@ -145,9 +143,7 @@ public List getResourceTemplateSpecifications var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); - var mcpResourceTemplate = McpSchema.ResourceTemplate.builder() - .uriTemplate(uri) - .name(name) + var mcpResourceTemplate = McpSchema.ResourceTemplate.builder(uri, name) .description(description) .mimeType(mimeType) .meta(meta) diff --git a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/changed/resource/AsyncMcpResourceListChangedMethodCallbackTests.java b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/changed/resource/AsyncMcpResourceListChangedMethodCallbackTests.java index c02807e870..d774b4de74 100644 --- a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/changed/resource/AsyncMcpResourceListChangedMethodCallbackTests.java +++ b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/changed/resource/AsyncMcpResourceListChangedMethodCallbackTests.java @@ -38,15 +38,11 @@ public class AsyncMcpResourceListChangedMethodCallbackTests { private static final List TEST_RESOURCES = List.of( - McpSchema.Resource.builder() - .uri("file:///test1.txt") - .name("test-resource-1") + McpSchema.Resource.builder("file:///test1.txt", "test-resource-1") .description("Test Resource 1") .mimeType("text/plain") .build(), - McpSchema.Resource.builder() - .uri("file:///test2.txt") - .name("test-resource-2") + McpSchema.Resource.builder("file:///test2.txt", "test-resource-2") .description("Test Resource 2") .mimeType("text/plain") .build()); diff --git a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/changed/resource/SyncMcpResourceListChangedMethodCallbackTests.java b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/changed/resource/SyncMcpResourceListChangedMethodCallbackTests.java index e3d1e364a4..fe3bf27351 100644 --- a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/changed/resource/SyncMcpResourceListChangedMethodCallbackTests.java +++ b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/changed/resource/SyncMcpResourceListChangedMethodCallbackTests.java @@ -36,15 +36,11 @@ public class SyncMcpResourceListChangedMethodCallbackTests { private static final List TEST_RESOURCES = List.of( - McpSchema.Resource.builder() - .uri("file:///test1.txt") - .name("test-resource-1") + McpSchema.Resource.builder("file:///test1.txt", "test-resource-1") .description("Test Resource 1") .mimeType("text/plain") .build(), - McpSchema.Resource.builder() - .uri("file:///test2.txt") - .name("test-resource-2") + McpSchema.Resource.builder("file:///test2.txt", "test-resource-2") .description("Test Resource 2") .mimeType("text/plain") .build()); diff --git a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/sampling/SamplingTestHelper.java b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/sampling/SamplingTestHelper.java index 84b3c5c7ae..258f6ac777 100644 --- a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/sampling/SamplingTestHelper.java +++ b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/sampling/SamplingTestHelper.java @@ -47,6 +47,7 @@ public static CreateMessageRequest createSampleRequest() { .modelPreferences(ModelPreferences.builder().addHint("claude-3-haiku").build()) .systemPrompt("You are a helpful assistant.") .temperature(0.7) + .maxTokens(100) .build(); } diff --git a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/changed/resource/AsyncMcpResourceListChangedProviderTests.java b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/changed/resource/AsyncMcpResourceListChangedProviderTests.java index 349f455eb9..9ee2b08a9e 100644 --- a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/changed/resource/AsyncMcpResourceListChangedProviderTests.java +++ b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/changed/resource/AsyncMcpResourceListChangedProviderTests.java @@ -38,15 +38,11 @@ public class AsyncMcpResourceListChangedProviderTests { private static final List TEST_RESOURCES = List.of( - McpSchema.Resource.builder() - .uri("file:///test1.txt") - .name("test-resource-1") + McpSchema.Resource.builder("file:///test1.txt", "test-resource-1") .description("Test Resource 1") .mimeType("text/plain") .build(), - McpSchema.Resource.builder() - .uri("file:///test2.txt") - .name("test-resource-2") + McpSchema.Resource.builder("file:///test2.txt", "test-resource-2") .description("Test Resource 2") .mimeType("text/plain") .build()); diff --git a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/changed/resource/SyncMcpResourceListChangedProviderTests.java b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/changed/resource/SyncMcpResourceListChangedProviderTests.java index 741b23a1be..9b9c89ad9a 100644 --- a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/changed/resource/SyncMcpResourceListChangedProviderTests.java +++ b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/changed/resource/SyncMcpResourceListChangedProviderTests.java @@ -36,15 +36,11 @@ public class SyncMcpResourceListChangedProviderTests { private static final List TEST_RESOURCES = List.of( - McpSchema.Resource.builder() - .uri("file:///test1.txt") - .name("test-resource-1") + McpSchema.Resource.builder("file:///test1.txt", "test-resource-1") .description("Test Resource 1") .mimeType("text/plain") .build(), - McpSchema.Resource.builder() - .uri("file:///test2.txt") - .name("test-resource-2") + McpSchema.Resource.builder("file:///test2.txt", "test-resource-2") .description("Test Resource 2") .mimeType("text/plain") .build()); diff --git a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpAsyncHandlersRegistryTests.java b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpAsyncHandlersRegistryTests.java index e240d533da..a84bfcb6ca 100644 --- a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpAsyncHandlersRegistryTests.java +++ b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpAsyncHandlersRegistryTests.java @@ -140,7 +140,11 @@ void elicitation() { registry.postProcessBeanFactory(beanFactory); registry.afterSingletonsInstantiated(); - var request = McpSchema.ElicitRequest.builder().message("Elicit request").progressToken("token-12345").build(); + var request = McpSchema.ElicitRequest.builder() + .message("Elicit request") + .requestedSchema(Map.of("type", "string")) + .progressToken("token-12345") + .build(); var response = registry.handleElicitation("client-1", request).block(); assertThat(response).isNotNull(); @@ -159,7 +163,11 @@ void missingElicitationHandler() { registry.postProcessBeanFactory(beanFactory); registry.afterSingletonsInstantiated(); - var request = McpSchema.ElicitRequest.builder().message("Elicit request").progressToken("token-12345").build(); + var request = McpSchema.ElicitRequest.builder() + .message("Elicit request") + .requestedSchema(Map.of("type", "string")) + .progressToken("token-12345") + .build(); assertThatThrownBy(() -> registry.handleElicitation("client-unknown", request).block()) .hasMessage("Elicitation not supported") .asInstanceOf(type(McpError.class)) @@ -181,6 +189,7 @@ void sampling() { var request = McpSchema.CreateMessageRequest.builder() .messages(List .of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Tell a joke")))) + .maxTokens(100) .build(); var response = registry.handleSampling("client-1", request).block(); @@ -204,6 +213,7 @@ void missingSamplingHandler() { var request = McpSchema.CreateMessageRequest.builder() .messages(List .of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Tell a joke")))) + .maxTokens(100) .build(); assertThatThrownBy(() -> registry.handleSampling("client-unknown", request).block()) .hasMessage("Sampling not supported") @@ -304,8 +314,8 @@ void resourceListChanged() { var handlers = beanFactory.getBean(HandlersConfiguration.class); List updatedResources = List.of( - McpSchema.Resource.builder().name("resource-1").uri("file:///resource/1").build(), - McpSchema.Resource.builder().name("resource-2").uri("file:///resource/2").build()); + McpSchema.Resource.builder("file:///resource/1", "resource-1").build(), + McpSchema.Resource.builder("file:///resource/2", "resource-2").build()); registry.handleResourceListChanged("client-1", updatedResources).block(); assertThat(handlers.getCalls()).hasSize(2) diff --git a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpSyncHandlersRegistryTests.java b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpSyncHandlersRegistryTests.java index be06eba044..99fbe1fbc0 100644 --- a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpSyncHandlersRegistryTests.java +++ b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpSyncHandlersRegistryTests.java @@ -139,7 +139,11 @@ void elicitation() { registry.postProcessBeanFactory(beanFactory); registry.afterSingletonsInstantiated(); - var request = McpSchema.ElicitRequest.builder().message("Elicit request").progressToken("token-12345").build(); + var request = McpSchema.ElicitRequest.builder() + .message("Elicit request") + .requestedSchema(Map.of("type", "string")) + .progressToken("token-12345") + .build(); var response = registry.handleElicitation("client-1", request); assertThat(response.content()).hasSize(1).containsEntry("message", "Elicit request"); @@ -155,7 +159,11 @@ void missingElicitationHandler() { registry.postProcessBeanFactory(beanFactory); registry.afterSingletonsInstantiated(); - var request = McpSchema.ElicitRequest.builder().message("Elicit request").progressToken("token-12345").build(); + var request = McpSchema.ElicitRequest.builder() + .message("Elicit request") + .requestedSchema(Map.of("type", "string")) + .progressToken("token-12345") + .build(); assertThatThrownBy(() -> registry.handleElicitation("client-unknown", request)) .hasMessage("Elicitation not supported") .asInstanceOf(type(McpError.class)) @@ -177,6 +185,7 @@ void sampling() { var request = McpSchema.CreateMessageRequest.builder() .messages(List .of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Tell a joke")))) + .maxTokens(100) .build(); var response = registry.handleSampling("client-1", request); @@ -198,6 +207,7 @@ void missingSamplingHandler() { var request = McpSchema.CreateMessageRequest.builder() .messages(List .of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Tell a joke")))) + .maxTokens(100) .build(); assertThatThrownBy(() -> registry.handleSampling("client-unknown", request)) .hasMessage("Sampling not supported") @@ -298,8 +308,8 @@ void resourceListChanged() { var handlers = beanFactory.getBean(HandlersConfiguration.class); List updatedResources = List.of( - McpSchema.Resource.builder().name("resource-1").uri("file:///resource/1").build(), - McpSchema.Resource.builder().name("resource-2").uri("file:///resource/2").build()); + McpSchema.Resource.builder("file:///resource/1", "resource-1").build(), + McpSchema.Resource.builder("file:///resource/2", "resource-2").build()); registry.handleResourceListChanged("client-1", updatedResources); assertThat(handlers.getCalls()).hasSize(2) diff --git a/mcp/transport/mcp-spring-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/transport/WebFluxSseClientTransport.java b/mcp/transport/mcp-spring-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/transport/WebFluxSseClientTransport.java index 0aa2f19c7b..208fb4b3b9 100644 --- a/mcp/transport/mcp-spring-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/transport/WebFluxSseClientTransport.java +++ b/mcp/transport/mcp-spring-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/transport/WebFluxSseClientTransport.java @@ -17,10 +17,15 @@ package org.springframework.ai.mcp.client.webflux.transport; import java.io.IOException; +import java.net.URI; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Function; +import io.modelcontextprotocol.client.transport.DefaultSseMessageEndpointValidator; +import io.modelcontextprotocol.client.transport.InvalidSseMessageEndpointException; +import io.modelcontextprotocol.client.transport.SseMessageEndpointValidator; import io.modelcontextprotocol.json.McpJsonDefaults; import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.json.TypeRef; @@ -146,7 +151,17 @@ public class WebFluxSseClientTransport implements McpClientTransport { * The SSE endpoint URI provided by the server. Used for sending outbound messages via * HTTP POST requests. */ - private String sseEndpoint; + private final String sseEndpoint; + + /** + * Used to capture the full SSE URI from the web client when connecting. + */ + private final AtomicReference sseUri = new AtomicReference<>(); + + /** + * Validator for the message endpoint. + */ + private final SseMessageEndpointValidator messageEndpointValidator; /** * Constructs a new SseClientTransport with the specified WebClient builder and @@ -170,13 +185,30 @@ public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, McpJsonMapp * @throws IllegalArgumentException if either parameter is null */ public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, McpJsonMapper jsonMapper, String sseEndpoint) { + this(webClientBuilder, jsonMapper, sseEndpoint, new DefaultSseMessageEndpointValidator()); + } + + /** + * Constructs a new SseClientTransport with the specified WebClient builder and + * ObjectMapper. Initializes both inbound and outbound message processing pipelines. + * @param webClientBuilder the WebClient.Builder to use for creating the WebClient + * instance + * @param jsonMapper the ObjectMapper to use for JSON processing + * @param sseEndpoint the SSE endpoint URI to use for establishing the connection + * @param messageEndpointValidator validator for the message endpoint + * @throws IllegalArgumentException if either parameter is null + */ + public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, McpJsonMapper jsonMapper, String sseEndpoint, + SseMessageEndpointValidator messageEndpointValidator) { Assert.notNull(jsonMapper, "jsonMapper must not be null"); Assert.notNull(webClientBuilder, "WebClient.Builder must not be null"); + Assert.notNull(messageEndpointValidator, "messageEndpointValidator must not be null"); Assert.hasText(sseEndpoint, "SSE endpoint must not be null or empty"); this.jsonMapper = jsonMapper; this.webClient = webClientBuilder.build(); this.sseEndpoint = sseEndpoint; + this.messageEndpointValidator = messageEndpointValidator; } @Override @@ -213,6 +245,14 @@ public Mono connect(Function, Mono> h this.inboundSubscription = events.concatMap(event -> Mono.just(event).handle((e, s) -> { if (ENDPOINT_EVENT_TYPE.equals(event.event())) { String messageEndpointUri = event.data(); + try { + this.messageEndpointValidator.validate(this.sseUri.get(), messageEndpointUri); + } + catch (InvalidSseMessageEndpointException ex) { + this.messageEndpointSink.tryEmitError(ex); + s.error(ex); + return; + } if (this.messageEndpointSink.tryEmitValue(messageEndpointUri).isSuccess()) { s.complete(); } @@ -298,8 +338,10 @@ protected Flux> eventStream() { // @formatter:off .uri(this.sseEndpoint) .accept(MediaType.TEXT_EVENT_STREAM) .header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION) - .retrieve() - .bodyToFlux(SSE_TYPE) + .exchangeToFlux(exchange -> { + this.sseUri.set(exchange.request().getURI()); + return exchange.bodyToFlux(SSE_TYPE); + }) .retryWhen(Retry.from(retrySignal -> retrySignal.handle(this.inboundRetryHandler))); } // @formatter:on @@ -384,6 +426,8 @@ public static class Builder { private @Nullable McpJsonMapper jsonMapper; + private SseMessageEndpointValidator messageEndpointValidator = new DefaultSseMessageEndpointValidator(); + /** * Creates a new builder with the specified WebClient.Builder. * @param webClientBuilder the WebClient.Builder to use @@ -415,13 +459,26 @@ public Builder jsonMapper(McpJsonMapper jsonMapper) { return this; } + /** + * Sets the validator that ensure the message endpoint returned over the SSE + * connection is valid. + * @param messageEndpointValidator the validator + * @return this builder + */ + public Builder messageEndpointValidator(SseMessageEndpointValidator messageEndpointValidator) { + Assert.notNull(messageEndpointValidator, "messageEndpointValidator must not be null"); + this.messageEndpointValidator = messageEndpointValidator; + return this; + } + /** * Builds a new {@link WebFluxSseClientTransport} instance. * @return a new transport instance */ public WebFluxSseClientTransport build() { return new WebFluxSseClientTransport(this.webClientBuilder, - this.jsonMapper == null ? McpJsonDefaults.getMapper() : this.jsonMapper, this.sseEndpoint); + this.jsonMapper == null ? McpJsonDefaults.getMapper() : this.jsonMapper, this.sseEndpoint, + this.messageEndpointValidator); } } diff --git a/mcp/transport/mcp-spring-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/transport/WebFluxSseClientTransportIT.java b/mcp/transport/mcp-spring-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/transport/WebFluxSseClientTransportIT.java index aeb92d2bc8..2e61914a54 100644 --- a/mcp/transport/mcp-spring-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/transport/WebFluxSseClientTransportIT.java +++ b/mcp/transport/mcp-spring-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/transport/WebFluxSseClientTransportIT.java @@ -16,12 +16,15 @@ package org.springframework.ai.mcp.client.webflux.transport; +import java.net.URI; import java.time.Duration; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; +import io.modelcontextprotocol.client.transport.InvalidSseMessageEndpointException; +import io.modelcontextprotocol.client.transport.SseMessageEndpointValidator; import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.McpSchema; @@ -33,6 +36,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.mockito.ArgumentCaptor; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import reactor.core.publisher.Flux; @@ -47,6 +51,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.matches; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for the {@link WebFluxSseClientTransport} class. @@ -69,6 +76,8 @@ class WebFluxSseClientTransportIT { private WebClient.Builder webClientBuilder; + private SseMessageEndpointValidator sseMessageEndpointValidator = mock(SseMessageEndpointValidator.class); + @BeforeAll static void startContainer() { container.start(); @@ -84,7 +93,8 @@ static void cleanup() { @BeforeEach void setUp() { this.webClientBuilder = WebClient.builder().baseUrl(host); - this.transport = new TestSseClientTransport(this.webClientBuilder, McpJsonMapperUtils.JSON_MAPPER); + this.transport = new TestSseClientTransport(this.webClientBuilder, McpJsonMapperUtils.JSON_MAPPER, + this.sseMessageEndpointValidator); this.transport.connect(Function.identity()).block(); } @@ -341,6 +351,47 @@ void testMessageOrderPreservation() { assertThat(this.transport.getInboundMessageCount()).isEqualTo(3); } + @Test + void testMessageEndpointValidation() throws InvalidSseMessageEndpointException { + var uriCaptor = ArgumentCaptor.forClass(URI.class); + verify(this.sseMessageEndpointValidator).validate(uriCaptor.capture(), + matches("/message\\?sessionId=[a-z0-9-]+")); + assertThat(uriCaptor.getValue().toString()).matches(host + "/sse"); + } + + @Test + void testMessageEndpointValidationRejects() { + TestSseClientTransport transport = new TestSseClientTransport(this.webClientBuilder, + McpJsonMapperUtils.JSON_MAPPER, (sseUri, messageEndpoint) -> { + throw new InvalidSseMessageEndpointException("boom", messageEndpoint); + }); + + try { + // fails to connect + StepVerifier.create(transport.connect(Function.identity())) + .verifyErrorMatches(WebFluxSseClientTransportIT::isInvalidEndpointError); + + // Since connection failed, there is no message endpoint, and no message can + // be sent + JSONRPCRequest testMessage = new JSONRPCRequest(McpSchema.JSONRPC_VERSION, "test-method", "test-id", + Map.of("key", "value")); + + StepVerifier.create(transport.sendMessage(testMessage)) + .verifyErrorMatches(WebFluxSseClientTransportIT::isInvalidEndpointError); + } + finally { + transport.closeGracefully(); + } + } + + private static boolean isInvalidEndpointError(Throwable e) { + if (e instanceof InvalidSseMessageEndpointException ismee) { + return ismee.getMessageEndpoint().matches("/message\\?sessionId=[a-z0-9-]+") + && ismee.getMessage().equals("boom"); + } + return false; + } + // Test class to access protected methods static final class TestSseClientTransport extends WebFluxSseClientTransport { @@ -348,8 +399,9 @@ static final class TestSseClientTransport extends WebFluxSseClientTransport { private Sinks.Many> events = Sinks.many().unicast().onBackpressureBuffer(); - private TestSseClientTransport(WebClient.Builder webClientBuilder, McpJsonMapper jsonMapper) { - super(webClientBuilder, jsonMapper); + private TestSseClientTransport(WebClient.Builder webClientBuilder, McpJsonMapper jsonMapper, + SseMessageEndpointValidator validator) { + super(webClientBuilder, jsonMapper, "/sse", validator); } @Override diff --git a/pom.xml b/pom.xml index 220b17104e..0e745aa60c 100644 --- a/pom.xml +++ b/pom.xml @@ -311,7 +311,7 @@ 5.5.6 - 2.0.0-M2 + 2.0.0-M3 4.13.1