When I used MCP, I simultaneously used Spring AOP, but a CGLIB proxy class was not generated for Tool. I tried many methods, but none of them worked.
there is my mcpToll codes
@Configuration
@Log4j2
@DependsOn("dataSourceAspect")
public class McpToolsConfig {
@Lazy
@Autowired
private ApplicationContext applicationContext;
@Bean
public ToolCallbackProvider functionRegistry() {
log.info("===== functionRegistry Bean 初始化开始 =====");
// 获取所有带有 @McpToolAnnotation 注解的Bean
Map<String, Object> annotatedBeans = applicationContext.getBeansWithAnnotation(McpToolAnnotation.class);
Collection<Object> tools = annotatedBeans.values();
tools.forEach(tool -> {
log.info("McpTool: {}, 代理状态: {}",
tool.getClass().getSimpleName(),
AopUtils.isAopProxy(tool));
});
return MethodToolCallbackProvider.builder()
.toolObjects(tools.toArray())
.build();
}
}
Comment From: tzolov
Hi @Lyeluo , can you please share a functional example or a test that reproduces the issue
Comment From: leelance
Why does my AOP proxy test work correctly? Is it because the way I’m testing it is incorrect?
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface McpToolAnnotation {
/**
* name
*
* @return mcp bean tool name
*/
String name();
/**
* mcp description
*
* @return description
*/
String description() default "";
}
@Component
@McpToolAnnotation(name = "weatherTool", description = "query weather tools")
public class WeatherTool {
@Tool(name = "getWeather", description = "Query the weather conditions of a city based on its name.")
public String getWeather(@ToolParam(description = "city name") String city) {
return city + ", It’s sunny today. ☀️";
}
@Transactional
@Tool(name = "getWeatherAsync", description = "Query the weather conditions of a city based on its name.")
public String getWeatherAsync(@ToolParam(description = "city name") String city) {
return city + ", It’s sunny today. ☀️";
}
}
@Component
@McpToolAnnotation(name = "mathTool", description = "calculate math tools")
public class MathTool {
@Tool(name = "add", description = "Calculate the sum of two numbers")
public int add(@ToolParam(description = "a") int a, @ToolParam(description = "b") int b) {
return a + b;
}
@Tool(name = "multiply", description = "Calculate the product of two numbers")
public int multiply(@ToolParam(description = "a") int a, @ToolParam(description = "b") int b) {
return a * b;
}
}
@Slf4j
@Configuration
@RequiredArgsConstructor
public class McpServerConfig {
private final ApplicationContext applicationContext;
@Bean
public ToolCallbackProvider mcpToolCallbackProvider() {
//get @McpToolAnnotation beans
Map<String, Object> annotatedBeans = applicationContext.getBeansWithAnnotation(McpToolAnnotation.class);
Collection<Object> tools = annotatedBeans.values();
tools.forEach(tool -> log.info("==>mcp tool bean: {}, aop: {}", tool.getClass().getSimpleName(), AopUtils.isAopProxy(tool)));
return MethodToolCallbackProvider.builder().toolObjects(tools.toArray()).build();
}
}
The JUnit test code is as follows:
@Slf4j
@SpringBootTest(properties = {"spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080"})
class WeatherToolTests extends AbstractMcpJunit {
@Autowired
private List<McpSyncClient> mcpSyncClients;
@Test
void getWeather() {
Map<String, Object> params = new HashMap<>();
params.put("city", "ShangHai");
List<McpSchema.TextContent> result = call(mcpSyncClients, "getWeather", params);
log.info("==>result: {}", result.get(0).text());
Assertions.assertNotNull(result);
}
@Test
void getWeatherAsync() {
Map<String, Object> params = new HashMap<>();
params.put("city", "Beijing");
List<McpSchema.TextContent> result = call(mcpSyncClients, "getWeatherAsync", params);
log.info("==>result: {}", result.get(0).text());
Assertions.assertNotNull(result);
}
}
The loaded MCP logs are as follows, and the AOP log prints true.
2025-09-23 15:19:00.255 INFO 22072 --- [ main] c.o.d.a.aiserver.config.McpServerConfig ---[ 36] : ==>mcp tool bean: MathTool, aop: false
2025-09-23 15:19:00.256 INFO 22072 --- [ main] c.o.d.a.aiserver.config.McpServerConfig ---[ 36] : ==>mcp tool bean: WeatherTool$$SpringCGLIB$$0, aop: true
2025-09-23 15:19:00.511 INFO 22072 --- [ main] o.s.a.m.s.a.McpServerAutoConfiguration ---[ 174] : Enable tools capabilities, notification: true
2025-09-23 15:19:00.555 INFO 22072 --- [ main] o.s.a.m.s.a.McpServerAutoConfiguration ---[ 174] : Registered tools: 4
2025-09-23 15:19:00.556 INFO 22072 --- [ main] o.s.a.m.s.a.McpServerAutoConfiguration ---[ 174] : Enable resources capabilities, notification: true
2025-09-23 15:19:00.556 INFO 22072 --- [ main] o.s.a.m.s.a.McpServerAutoConfiguration ---[ 174] : Enable prompts capabilities, notification: true
Comment From: Lyeluo
Hi @Lyeluo , can you please share a functional example or a test that reproduces the issue
ok, that's my aspect config code
@Log4j2
@Aspect
@Component("dataSourceAspect")
public class DataSourceAspect {
@Autowired
private TenantDatasourceConfig tenantDatasourceConfig;
// 添加构造方法,打印初始化日志
public DataSourceAspect() {
log.info("===== DataSourceAspect 切面Bean开始初始化 =====");
}
@Pointcut("@annotation(tenantIdSwitch)")
public void pointcut(TenantIdSwitch tenantIdSwitch) {
}
@Before("pointcut(tenantIdSwitch)")
public void before(JoinPoint joinPoint, TenantIdSwitch tenantIdSwitch) throws Throwable {
Object[] args = joinPoint.getArgs();
int paramIndex = tenantIdSwitch.tenantIdIndex();
if (paramIndex >= 0 && paramIndex < args.length) {
Object param = args[paramIndex];
String tenantId = null;
if (param instanceof BaseVO) {
tenantId = ((BaseVO) param).getTenantId();
} else if (param instanceof String) {
tenantId = String.valueOf(args[paramIndex]);
}
if (!StrUtil.isEmpty(tenantId)) {
String datasourceName = getDatasourceByTenantId(tenantId);
if (datasourceName != null) {
DynamicDataSourceContextHolder.push(datasourceName);
}
}
}
}
}
that's my service code
@McpToolAnnotation
@Service
public class KTTools {
@Resource
private KTService ktService;
@TenantIdSwitch(tenantIdIndex = 0)
@Tool(name = "get_kt_info", description = """
查询知识点基础信息,返回以下数据:
1. 知识点名称
2. 知识点编码
3. 知识点难易度
4. 知识点掌握度
5. 任务名称
6. 关联的知识点信息
7. 与关联知识点的关系
""")
public List<KtResRelVO> getKtInfo(@ToolParam(description = "租户id") String tenantId, @ToolParam(description = "学期ID") String termId,
@ToolParam(description = "基础课程ID") String baseCourseId, @ToolParam(description = "教学班级ID") String teachClassId,
@ToolParam(description = "数据版本号集合") List<String> versionId, @ToolParam(description = "知识点名称集合", required = false) List<String> ktNameList,
@ToolParam(description = "知识点标签", required = false) String ktTags, @ToolParam(description = "知识点难易度", required = false) String ktDifficulty,
@ToolParam(description = "知识点掌握度", required = false) String ktMastery, @ToolParam(description = "知识点分类", required = false) String ktClassifyCode,
@ToolParam(description = "前修知识点集合", required = false) List<String> preKtNameList, @ToolParam(description = "关联知识点id", required = false) String ktRelatedId,
@ToolParam(description = "知识点关联资源名称", required = false) String resName, @ToolParam(description = "知识点关联的资源类型(有以下类型:学习资料,测验,作业,项目,考试,教学资料,试题)", required = false) String resType,
@ToolParam(description = "知识点ID集合", required = false) List<String> ktIdList) {
return ktService.getKtInfo(tenantId, termId, baseCourseId, teachClassId, versionId, ktNameList, ktTags, ktDifficulty, ktMastery, ktClassifyCode, preKtNameList, ktRelatedId, resName, resType, ktIdList);
}
}
and i have already configured that it must use cglib like this
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
public class McpServerApplication {
I found when i start my project using intelliJ IDEA, the pointcut can run correct, when i package with maven and use the same config , the 'before poincut' will not run .