feat(mcp): Add @McpClient qualifier for selective client injection
#5202
+1,010
−79
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
feat(mcp): Add
@McpClientqualifier for selective client injectionCloses #5201
Summary
This PR adds a
@McpClient("connectionName")qualifier annotation that enables selective injection of individual MCP client beans (bothMcpSyncClientandMcpAsyncClient), addressing the need for multi-agent systems where different agents require different MCP servers.Changes
New Files
mcp/common/.../McpClient.javaauto-configurations/.../McpSyncClientFactory.javaauto-configurations/.../NamedMcpSyncClientFactoryBean.javaauto-configurations/.../McpAsyncClientFactory.javaauto-configurations/.../NamedMcpAsyncClientFactoryBean.javaauto-configurations/.../McpConnectionBeanRegistrar.javaBeanDefinitionRegistryPostProcessorfor early bean registrationModified Files
McpClientAutoConfiguration.javaMcpSyncClientFactory,McpAsyncClientFactory, and registrar beans for individual client injectionBefore & After
List<McpSyncClient> all@McpClient("name") McpSyncClientList<McpAsyncClient> all@McpClient("name") McpAsyncClientNote: The existing
List<McpSyncClient>andList<McpAsyncClient>beans remain unchanged. This PR adds an additional injection method, not a replacement.Usage
Sync Mode (default)
Async Mode
spring.ai.mcp.client.type=ASYNCTechnical Considerations
1. FactoryBean + BeanDefinitionRegistryPostProcessor Pattern
I tried simpler approaches first, but they all failed:
@Bean Map<String, McpSyncClient>@Qualifierresolution targets individual beans in the context, not entries within a Map-typed bean. This is by design—@QualifierusesBeanFactory.getBean(type, qualifier), not Map lookup.beanFactory.registerSingleton()BeanDefinition, preventing attachment of the custom@McpClientqualifier metadata. Also requires immediate object instantiation, which is impossible before dependencies are ready.BeanDefinitionRegistryPostProcessorruns before regular beans are instantiated. Dependencies likeMcpSyncClientConfigurerandClientMcpSyncHandlersRegistryare not yet available.The FactoryBean pattern solves this by:
2. Setter Injection in FactoryBean
The
McpConnectionBeanRegistraronly knows theconnectionNameat registration time. Complex dependencies likeMcpSyncClientConfigurer,McpClientCommonProperties, andClientMcpSyncHandlersRegistryare not available yet. Setter injection solves this:3. Centralized Client Creation Logic
I introduced
McpSyncClientFactoryandMcpAsyncClientFactoryto encapsulate all client creation logic. This eliminates code duplication and ensures consistent configuration.Separation of concerns:
4. Conditional Sync/Async Bean Registration
McpConnectionBeanRegistrarreadsspring.ai.mcp.client.typeand registers the appropriate FactoryBean:5. Dual Qualifier Registration
Both
@McpClientand@Qualifiermetadata are registered for flexibility:Both annotations are therefore enabled by this PR, which registers individual named beans.
@McpClient("name")@Qualifier("name")6. Static Bean Method for Registrar
The registrar bean method is
staticbecauseBeanDefinitionRegistryPostProcessorbeans must be registered very early in the Spring lifecycle. Static@Beanmethods are processed before instance methods, ensuring the registrar runs before other beans are created.7. No Explicit destroyMethod
I initially set
destroyMethod("close"), but it causedBeanDefinitionValidationException:Spring validates
destroyMethodagainst the FactoryBean class, not the produced object. SinceMcpSyncClientandMcpAsyncClientareAutoCloseable, Spring automatically infers and callsclose()on the created client (no explicit configuration needed).Testing
Integration Test:
Sample Configuration:
Bean Registration Log(Sync):

Bean Registration Log(Async):
