-
Notifications
You must be signed in to change notification settings - Fork 84
Feat/mcp #305
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
base: develop
Are you sure you want to change the base?
Feat/mcp #305
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.tinyengine.it.mcp.annotation; | ||
|
|
||
| import java.lang.annotation.*; | ||
|
|
||
| /** | ||
| * The interface MCPTool. MCP工具注解,用于标识MCP工具方法,提供工具名称和描述信息。 | ||
| */ | ||
| @Target(ElementType.METHOD) | ||
| @Retention(RetentionPolicy.RUNTIME) | ||
| @Documented | ||
| public @interface MCPTool { | ||
| String name() default ""; | ||
| String description() default ""; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.tinyengine.it.mcp.config; | ||
|
|
||
| import com.tinyengine.it.mcp.core.StreamableMcpClient; | ||
| import com.tinyengine.it.mcp.tools.DemoMcpTools; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.ai.tool.ToolCallbackProvider; | ||
| import org.springframework.ai.tool.method.MethodToolCallbackProvider; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.context.annotation.Primary; | ||
|
|
||
| @Configuration | ||
| public class McpToolsConfig { | ||
| private static final Logger log = LoggerFactory.getLogger(McpToolsConfig.class); | ||
|
|
||
|
|
||
| /** | ||
| * 方式一:通过 ToolCallbackProvider 批量注册工具 | ||
| */ | ||
| @Bean | ||
| @Primary | ||
| public ToolCallbackProvider toolCallbackProvider(DemoMcpTools demoMcpTools) { | ||
|
|
||
|
|
||
| MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder() | ||
| .toolObjects(demoMcpTools) | ||
| .build(); | ||
| // 立即验证 | ||
| log.info("Tool count from provider: " + provider.getToolCallbacks().length); | ||
| return provider; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.tinyengine.it.mcp.core; | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import lombok.Data; | ||
|
|
||
| import java.lang.reflect.Method; | ||
|
|
||
| @Data | ||
| public class MCPToolDefinition { | ||
| private String name; | ||
| private String description; | ||
| private Object bean; | ||
| private Method method; | ||
| private JsonNode inputSchema; // JSON Schema 格式 | ||
|
|
||
| /** | ||
| * 执行工具方法 | ||
| * @param arguments | ||
| * @return | ||
| * @throws Exception | ||
| */ | ||
| public Object execute(JsonNode arguments) throws Exception { | ||
| // 参数转换:根据方法参数类型从 JsonNode 中提取值 | ||
| // 这里简化:假设方法只有一个参数,且为 JsonNode 或 String | ||
| Class<?>[] paramTypes = method.getParameterTypes(); | ||
| Object[] args = new Object[paramTypes.length]; | ||
| for (int i = 0; i < paramTypes.length; i++) { | ||
| Class<?> type = paramTypes[i]; | ||
| if (type == JsonNode.class) { | ||
| args[i] = arguments; | ||
| } else if (type == String.class) { | ||
| args[i] = arguments.toString(); | ||
| } else { | ||
| // 可以扩展更复杂的转换,例如使用 ObjectMapper 转换为指定类型 | ||
| args[i] = null; | ||
| } | ||
|
Comment on lines
+27
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Null fallback breaks typed/primitive tool parameters at runtime. For non- 🐛 Proposed fix+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.lang.reflect.Parameter;
...
+ private static final ObjectMapper MAPPER = new ObjectMapper();
...
public Object execute(JsonNode arguments) throws Exception {
- Class<?>[] paramTypes = method.getParameterTypes();
- Object[] args = new Object[paramTypes.length];
- for (int i = 0; i < paramTypes.length; i++) {
- Class<?> type = paramTypes[i];
- if (type == JsonNode.class) {
- args[i] = arguments;
- } else if (type == String.class) {
- args[i] = arguments.toString();
- } else {
- // 可以扩展更复杂的转换,例如使用 ObjectMapper 转换为指定类型
- args[i] = null;
- }
- }
+ Parameter[] params = method.getParameters();
+ Object[] args = new Object[params.length];
+ for (int i = 0; i < params.length; i++) {
+ Class<?> type = params[i].getType();
+ JsonNode valueNode = (arguments != null && arguments.isObject())
+ ? arguments.get(params[i].getName())
+ : arguments;
+ if (valueNode == null || valueNode.isNull()) {
+ if (type.isPrimitive()) {
+ throw new IllegalArgumentException("Missing required arg: " + params[i].getName());
+ }
+ args[i] = null;
+ } else if (type == JsonNode.class) {
+ args[i] = valueNode;
+ } else {
+ args[i] = MAPPER.convertValue(valueNode, type);
+ }
+ }
return method.invoke(bean, args);
}🤖 Prompt for AI Agents |
||
| } | ||
| return method.invoke(bean, args); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package com.tinyengine.it.mcp.core; | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.fasterxml.jackson.databind.node.ObjectNode; | ||
| import com.tinyengine.it.mcp.annotation.MCPTool; | ||
|
|
||
| import org.springframework.context.ApplicationContext; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.lang.reflect.Method; | ||
| import java.lang.reflect.Parameter; | ||
| import java.util.Collection; | ||
| import java.util.Map; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
|
|
||
| @Component | ||
| public class MCPToolRegistry { | ||
| private final Map<String, MCPToolDefinition> tools = new ConcurrentHashMap<>(); | ||
| private final ObjectMapper objectMapper = new ObjectMapper(); | ||
| private final ApplicationContext applicationContext; | ||
| private volatile boolean initialized = false; | ||
|
|
||
| public MCPToolRegistry(ApplicationContext applicationContext) { | ||
| this.applicationContext = applicationContext; | ||
| } | ||
|
|
||
| private void initialize() { | ||
| if (initialized) return; | ||
| synchronized (this) { | ||
| if (initialized) return; | ||
| // 扫描所有 bean | ||
| Map<String, Object> beans = applicationContext.getBeansWithAnnotation(org.springframework.stereotype.Component.class); | ||
| for (Object bean : beans.values()) { | ||
| for (Method method : bean.getClass().getDeclaredMethods()) { | ||
| if (method.isAnnotationPresent(MCPTool.class)) { | ||
| registerTool(bean, method); | ||
|
Comment on lines
+35
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -name "MCPToolRegistry.java" -type fRepository: opentiny/tiny-engine-backend-java Length of output: 146 🏁 Script executed: cat -n ./base/src/main/java/com/tinyengine/it/mcp/core/MCPToolRegistry.javaRepository: opentiny/tiny-engine-backend-java Length of output: 3179 🏁 Script executed: # Check if there are any other places where beans are scanned or if there's any proxy handling
rg "AopUtils|getTargetClass|getBeansWithAnnotation" --type java
# Check if beans in this context are actually proxied (look for `@Transactional`, `@Async`, etc.)
rg "@Transactional|@Async|@Cacheable" base/src/main/java/com/tinyengine/it/mcp/core/ --type java
# Check imports to see if AopUtils is already imported elsewhere
rg "import.*AopUtils" --type javaRepository: opentiny/tiny-engine-backend-java Length of output: 266 🏁 Script executed: # Check if there are beans with proxy-creating annotations anywhere in the project
# that might be picked up by getBeansWithAnnotation(Component.class)
rg "@Transactional|@Async|@Cacheable" base/src/main/java/com/tinyengine/it/mcp/ --type java -A 5
# Check the MCPTool annotation definition to understand its scope
find . -name "MCPTool.java" -type fRepository: opentiny/tiny-engine-backend-java Length of output: 144 🏁 Script executed: cat -n ./base/src/main/java/com/tinyengine/it/mcp/annotation/MCPTool.javaRepository: opentiny/tiny-engine-backend-java Length of output: 485 🏁 Script executed: # Check if there are any Component beans in the mcp module that might be proxied
find ./base/src/main/java/com/tinyengine/it/mcp -name "*.java" -type f -exec grep -l "@Component\|@Service\|@Repository" {} \;
# Check if spring-aop is in the dependencies
fd -e "pom.xml" -e "build.gradle" | head -5 | xargs grep -l "spring-aop" 2>/dev/null || echo "spring-aop dependency check..."Repository: opentiny/tiny-engine-backend-java Length of output: 400 🏁 Script executed: # Check the actual component beans to see if any have proxy-creating annotations
cat -n ./base/src/main/java/com/tinyengine/it/mcp/tools/DemoMcpTools.java
# Also check StreamableHttpClientManager
cat -n ./base/src/main/java/com/tinyengine/it/mcp/core/StreamableHttpClientManager.javaRepository: opentiny/tiny-engine-backend-java Length of output: 6630 🏁 Script executed: # Check pom.xml for spring-aop dependency
find . -name "pom.xml" -type f | xargs grep -A 5 -B 5 "spring-aop" | head -30
# Also check if spring-aop is listed without being explicitly found
cat pom.xml | head -100Repository: opentiny/tiny-engine-backend-java Length of output: 4158 🏁 Script executed: # Check if any of the Component beans in the MCP module have proxy-creating annotations
rg "@Transactional|@Async|@Cacheable|@CircuitBreaker" base/src/main/java/com/tinyengine/it/mcp/ --type java
# Check the full pom.xml to see what Spring version and dependencies are being used
grep -A 20 "spring-boot-starter-aop" pom.xmlRepository: opentiny/tiny-engine-backend-java Length of output: 810 🏁 Script executed: # Check all Component/Service/Repository beans in the entire project for proxy-creating annotations
rg "@Transactional|@Async|@Cacheable|@CircuitBreaker" base/src/main/java/com/tinyengine/it/ --type java | grep -v ".class"
# Check for aspect definitions or aspect components
rg "@Aspect|@EnableAspectJAutoProxy" base/src/main/java/ --type java
# Let's also verify what happens with getTargetClass on non-proxied beans
# by checking Spring documentation expectations
grep -r "getTargetClass" base/src/main/java/ || echo "No AopUtils usage found"Repository: opentiny/tiny-engine-backend-java Length of output: 936 🏁 Script executed: # Check GitFileReaderService to see if it has any proxy-creating annotations
cat -n ./base/src/main/java/com/tinyengine/it/mcp/tools/GitFileReaderService.java | head -40Repository: opentiny/tiny-engine-backend-java Length of output: 1717 🏁 Script executed: # Let's verify the Spring documentation expectation about AopUtils.getTargetClass
# Check if there's any documentation or examples in the codebase
rg "getDeclaredMethods|getTargetClass" --type java | head -20
# Let's also check what happens when we use getClass() on a proxied bean
# by looking at Spring Boot documentation
echo "Checking if this is a best practice issue or active bug..."Repository: opentiny/tiny-engine-backend-java Length of output: 268 🏁 Script executed: # Verify that AopUtils is available in the Spring version being used
grep -A 2 "spring-boot-starter-aop" pom.xml
# Check if org.springframework.aop.support.AopUtils is part of spring-aop
# by looking at what's imported from Spring in the project
rg "org.springframework.aop" base/src/main/java/ --type java | head -10Repository: opentiny/tiny-engine-backend-java Length of output: 161 Use When scanning for ♻️ Suggested fix+import org.springframework.aop.support.AopUtils;
...
- for (Object bean : beans.values()) {
- for (Method method : bean.getClass().getDeclaredMethods()) {
+ for (Object bean : beans.values()) {
+ Class<?> targetClass = AopUtils.getTargetClass(bean);
+ for (Method method : targetClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(MCPTool.class)) {
registerTool(bean, method);
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| } | ||
| } | ||
| initialized = true; | ||
| } | ||
| } | ||
|
|
||
| public MCPToolDefinition getTool(String name) { | ||
| initialize(); | ||
| return tools.get(name); | ||
| } | ||
|
|
||
| public Collection<MCPToolDefinition> getAllTools() { | ||
| initialize(); | ||
| return tools.values(); | ||
| } | ||
|
|
||
| private void registerTool(Object bean, Method method) { | ||
| MCPTool annotation = method.getAnnotation(MCPTool.class); | ||
| MCPToolDefinition def = new MCPToolDefinition(); | ||
| def.setName(annotation.name().isEmpty() ? method.getName() : annotation.name()); | ||
| def.setDescription(annotation.description()); | ||
| def.setBean(bean); | ||
| def.setMethod(method); | ||
| def.setInputSchema(generateSchema(method)); | ||
| tools.put(def.getName(), def); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tool name collisions silently overwrite existing registrations.
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| private JsonNode generateSchema(Method method) { | ||
| ObjectNode schema = objectMapper.createObjectNode(); | ||
| schema.put("type", "object"); | ||
| ObjectNode properties = objectMapper.createObjectNode(); | ||
| for (Parameter param : method.getParameters()) { | ||
| String paramName = param.getName(); | ||
| ObjectNode prop = objectMapper.createObjectNode(); | ||
| // 简化:假设参数都是 String | ||
| prop.put("type", "string"); | ||
| properties.set(paramName, prop); | ||
|
Comment on lines
+71
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generated JSON schema currently misrepresents parameter types. All params are declared as 🤖 Prompt for AI Agents |
||
| } | ||
| schema.set("properties", properties); | ||
| return schema; | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.tinyengine.it.mcp.core; | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import lombok.Data; | ||
|
|
||
| @Data | ||
| public class RemoteToolDefinition { | ||
| private String name; | ||
| private String description; | ||
| private JsonNode parameters; // JSON Schema | ||
| private String serverUrl; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not ship a placeholder API key in an active profile.
This makes the MCP/OpenAI path boot with an invalid credential and fail only on first use. Bind it from the environment instead so misconfiguration is caught at deploy time.
Suggested fix
🤖 Prompt for AI Agents