Skip to content
Open
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
11 changes: 10 additions & 1 deletion app/src/main/java/com/tinyengine/it/TinyEngineApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,24 @@
package com.tinyengine.it;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import java.util.Map;

/**
* The type Tiny engine application.
*
* @since 2024-10-20
*/
@SpringBootApplication
@EnableAspectJAutoProxy
@MapperScan({"com.tinyengine.it.mapper","com.tinyengine.it.dynamic.dao"})
@MapperScan({"com.tinyengine.it.mapper","com.tinyengine.it.dynamic.dao","com.tinyengine.it.mcp.tools"})
public class TinyEngineApplication {
/**
* The entry point of application.
Expand All @@ -34,4 +40,7 @@ public class TinyEngineApplication {
public static void main(String[] args) {
SpringApplication.run(TinyEngineApplication.class, args);
}



}
34 changes: 34 additions & 0 deletions app/src/main/resources/application-alpha.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ spring:
min-evictable-idle-time-millis: 300000 # 连接在池中保持空闲的最小时间(单位:毫秒)。如果空闲时间超过这个值,连接将被回收,默认值为 1800000。
pool-prepared-statements: true # 是否缓存 PreparedStatement 对象,默认值为 true。
max-open-prepared-statements: 20 # 最大缓存的 PreparedStatement 数量,默认值为 -1,表示无限制。如果 `pool-prepared-statements` 设置为 true,设置此值以限制缓存数量。
ai:
mcp:
server:
enabled: true
protocol: STREAMABLE
version: 1.0.0
tool-change-notification: true
type: SYNC
annotation-scanner:
enabled: false
capabilities:
tool: true
streamable-http:
mcp-endpoint: /mcp
name: tiny-engine-mcp



# 清空任务配置
cleanup:
enabled: false
Expand Down Expand Up @@ -89,3 +107,19 @@ logging:
cors:
allowed-origins: "*"


openai:
api-key: your-api-key
base-url: https://api.deepseek.com/v1/chat/completions
default-model: deepseek-chat
timeout-seconds: 300
Comment on lines +111 to +115
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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
 openai:
-  api-key: your-api-key
+  api-key: ${OPENAI_API_KEY}
   base-url: https://api.deepseek.com/v1/chat/completions
   default-model: deepseek-chat
   timeout-seconds: 300
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/resources/application-alpha.yml` around lines 111 - 115, The
openai.api-key is currently a hardcoded placeholder; replace it with an
environment-bound value so deployments fail fast if missing by changing the
openai.api-key entry to read from an environment/property placeholder (e.g. use
a Spring property-style placeholder like ${OPENAI_API_KEY} with no default) and
ensure any boot-time validation uses that property so the app errors at startup
when the key is not set; update any code reading openai.api-key to expect the
env-bound value and remove the literal "your-api-key".

streamable-tools:
doc_tools: https://agent.opentiny.design/servers/doc-tools-mcp/mcp # 工具名 -> SSE服务URL


streamable-http:
max-connections: 10
connection-timeout-seconds: 30
request-timeout-seconds: 60
protocol-version: 2025-06-18

91 changes: 87 additions & 4 deletions app/src/main/resources/sql/mysql/init_data_for_test_v1.0.0.sql

Large diffs are not rendered by default.

24 changes: 22 additions & 2 deletions base/src/main/java/com/tinyengine/it/config/OpenAIConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,38 @@
package com.tinyengine.it.config;

import lombok.Data;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

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

/**
* The type Open AI config.
*
* @since 2025-08-06
*/
@Data
@Configuration
@Component
@ConfigurationProperties(prefix = "openai")
public class OpenAIConfig {
private String apiKey = "your-api-key";
private String baseUrl = "https://api.deepseek.com/chat/completions";
private String defaultModel = "deepseek-chat";
private int timeoutSeconds = 300;

// Streamable HTTP 工具映射(工具名 -> 服务URL)
private Map<String, String> streamableTools = new HashMap<>();

// Streamable HTTP 连接池配置
private StreamableHttpConfig streamableHttp = new StreamableHttpConfig();

@Data
public static class StreamableHttpConfig {
private int maxConnections = 20;
private int connectionTimeoutSeconds = 30;
private int requestTimeoutSeconds = 60;
private String protocolVersion = "2025-06-18"; // 协议版本
private boolean keepAlive = true;
}
}
14 changes: 14 additions & 0 deletions base/src/main/java/com/tinyengine/it/mcp/annotation/MCPTool.java
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Null fallback breaks typed/primitive tool parameters at runtime.

For non-String/JsonNode params, this sets args[i] = null, which will fail for primitive parameters and many typed methods when invoked reflectively.

🐛 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
Verify each finding against the current code and only fix it if needed.

In `@base/src/main/java/com/tinyengine/it/mcp/core/MCPToolDefinition.java` around
lines 27 - 36, The current reflective arg resolution in MCPToolDefinition sets
args[i]=null for any param type other than JsonNode or String, which breaks
primitive/typed parameters; update the logic in the method handling
paramTypes/args/arguments to convert the JsonNode (arguments) into the target
parameter type using an ObjectMapper.convertValue call (e.g.,
convertValue(arguments, targetType)), and if paramTypes[i].isPrimitive() first
map the primitive to its wrapper class (or implement a small primitive->wrapper
map) so convertValue and Method.invoke receive the correct boxed type; ensure
you replace the null fallback with this conversion path for non-String/JsonNode
types.

}
return method.invoke(bean, args);
}
}
82 changes: 82 additions & 0 deletions base/src/main/java/com/tinyengine/it/mcp/core/MCPToolRegistry.java
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "MCPToolRegistry.java" -type f

Repository: opentiny/tiny-engine-backend-java

Length of output: 146


🏁 Script executed:

cat -n ./base/src/main/java/com/tinyengine/it/mcp/core/MCPToolRegistry.java

Repository: 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 java

Repository: 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 f

Repository: opentiny/tiny-engine-backend-java

Length of output: 144


🏁 Script executed:

cat -n ./base/src/main/java/com/tinyengine/it/mcp/annotation/MCPTool.java

Repository: 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.java

Repository: 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 -100

Repository: 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.xml

Repository: 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 -40

Repository: 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 -10

Repository: opentiny/tiny-engine-backend-java

Length of output: 161


Use AopUtils.getTargetClass() to handle Spring proxies in method discovery.

When scanning for @MCPTool annotations, bean.getClass().getDeclaredMethods() fails for proxied beans (e.g., those with @Transactional). Since this registry scans arbitrary @Component beans, use AopUtils.getTargetClass(bean) to safely resolve the actual class regardless of proxy type.

♻️ 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
Verify each finding against the current code and only fix it if needed.

In `@base/src/main/java/com/tinyengine/it/mcp/core/MCPToolRegistry.java` around
lines 35 - 37, The loop in MCPToolRegistry that inspects
bean.getClass().getDeclaredMethods() misses methods on proxied Spring beans;
change it to resolve the real class via AopUtils.getTargetClass(bean) and
iterate that class's declared methods, then call registerTool(bean, method) as
before; also add the necessary import for
org.springframework.aop.support.AopUtils and ensure you keep using the original
bean instance when invoking registerTool so proxy behavior is preserved.

}

}
}
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Tool name collisions silently overwrite existing registrations.

tools.put(def.getName(), def) replaces previous entries without warning, which can route calls to the wrong implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@base/src/main/java/com/tinyengine/it/mcp/core/MCPToolRegistry.java` at line
64, The current registration call tools.put(def.getName(), def) in
MCPToolRegistry silently overwrites existing entries; change the registration
flow to detect collisions by checking tools.containsKey(def.getName()) before
calling tools.put, and if a collision exists either throw an
IllegalStateException (including def.getName() and both implementations'
identifying info) or log an error and skip/abort the registration to avoid
replacing the original; update the code path that registers via tools.put to
perform this guard and ensure callers of register/put handle the thrown
exception or error return.

}

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Generated JSON schema currently misrepresents parameter types.

All params are declared as "type": "string", which conflicts with methods expecting numbers/objects and leads to invalid tool-call payloads.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@base/src/main/java/com/tinyengine/it/mcp/core/MCPToolRegistry.java` around
lines 71 - 76, The loop currently hardcodes every parameter as "string"; change
it to inspect each parameter's runtime type via param.getType() and set an
appropriate JSON Schema type on the ObjectNode (e.g., if type == String.class =>
"string"; if primitive/boxed int/long => "integer"; if primitive/boxed
float/double => "number"; if boolean/Boolean => "boolean"; if array or
Collection => "array"; if Map or other complex types => "object"). Use the same
objectMapper.createObjectNode() and properties.set(paramName, prop) but replace
the fixed prop.put("type", "string") with logic mapping method.getParameters()
-> param.getType() -> schema type (handle boxed vs primitive and common
collection/map cases). Ensure MCPToolRegistry's parameter handling uses this
mapping so payload types match the method signature.

}
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;
}
Loading