From 3e25f370b8d9cd544cd66f1a71b319db3372de8c Mon Sep 17 00:00:00 2001 From: chenchuchuan <13554600537@163.com> Date: Tue, 1 Sep 2026 16:29:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(mcp):=20=E6=B7=BB=E5=8A=A0MCP=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=A1=86=E6=9E=B6=E6=94=AF=E6=8C=81=E5=8C=85=E6=8B=AC?= =?UTF-8?q?=E6=B5=81=E5=BC=8FHTTP=E4=BC=A0=E8=BE=93=E5=92=8C=E6=B3=A8?= =?UTF-8?q?=E8=A7=A3=E6=89=AB=E6=8F=8F=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 McpStreamableController 提供流式HTTP单端点支持 /tenantadmin/mcp - 实现 StreamableHttpServerTransportProvider 支持MCP协议的流式HTTP传输 - 添加 @McpToolClass 和 @McpToolParam 注解用于标记MCP工具方法和参数 - 开发 McpToolScanner 扫描器自动注册带有注解的工具方法 - 集成 mcp-java8-sdk 依赖支持MCP协议服务器功能 - 新增 McpToolArgInfo 和 McpToolParamType 模型类处理工具参数元信息 - 配置 McpToolConfig 自动装配SSE和流式HTTP两种传输方式 - 为 MyPageParam 添加 MCP 参数注解支持分页参数映射 --- common/common-core/pom.xml | 15 + .../common/core/mcp/annotation/McpToolClass.java | 30 ++ .../common/core/mcp/annotation/McpToolParam.java | 48 ++ .../common/core/mcp/config/McpToolConfig.java | 91 ++++ .../common/core/mcp/config/McpToolScanner.java | 499 +++++++++++++++++++++ .../mcp/controller/McpStreamableController.java | 47 ++ .../common/core/mcp/model/McpToolArgInfo.java | 71 +++ .../common/core/mcp/model/McpToolParamType.java | 35 ++ .../StreamableHttpServerTransportProvider.java | 230 ++++++++++ .../apelet/common/core/object/MyPageParam.java | 5 + .../online/controller/OnlineColumnController.java | 8 +- .../OnlineDatasourceRelationController.java | 8 +- .../online/controller/OnlineFormController.java | 21 +- .../online/controller/OnlinePageController.java | 32 +- .../apelet/common/online/dto/OnlineColumnDto.java | 27 ++ .../common/online/dto/OnlineDatasourceDto.java | 8 + .../online/dto/OnlineDatasourceRelationDto.java | 14 + .../apelet/common/online/dto/OnlineFormDto.java | 14 + .../apelet/common/online/dto/OnlinePageDto.java | 11 + 19 files changed, 1193 insertions(+), 21 deletions(-) create mode 100644 common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolClass.java create mode 100644 common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolParam.java create mode 100644 common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolConfig.java create mode 100644 common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolScanner.java create mode 100644 common/common-core/src/main/java/apelet/common/core/mcp/controller/McpStreamableController.java create mode 100644 common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolArgInfo.java create mode 100644 common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolParamType.java create mode 100644 common/common-core/src/main/java/apelet/common/core/mcp/transport/StreamableHttpServerTransportProvider.java diff --git a/common/common-core/pom.xml b/common/common-core/pom.xml index 3639cf8..8c32b92 100644 --- a/common/common-core/pom.xml +++ b/common/common-core/pom.xml @@ -117,5 +117,20 @@ 4.4.0 compile + + + com.github.krrr + mcp-java8-sdk + 0.8.1 + + + + + jitpack.io + https://jitpack.io + true + true + + diff --git a/common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolClass.java b/common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolClass.java new file mode 100644 index 0000000..6eb2f1e --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolClass.java @@ -0,0 +1,30 @@ +package apelet.common.core.mcp.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * MCP 工具方法注解:标注在业务方法上,启动时由 McpToolScanner 扫描自动生成 MCP Tool 并注册。 + *

name 与 description 即 MCP Tool 的 name / description,inputSchema 由方法参数上的 @McpToolParam 组装。

+ * + * @author chenchuchuan + * @date 2026-08-31 + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface McpToolClass { + + /** + * MCP Tool 名称,客户端 tools/list 看到的 name。 + */ + String name(); + + /** + * MCP Tool 描述,供大模型理解用途。 + */ + String description(); +} diff --git a/common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolParam.java b/common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolParam.java new file mode 100644 index 0000000..de219e7 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolParam.java @@ -0,0 +1,48 @@ +package apelet.common.core.mcp.annotation; + +import apelet.common.core.mcp.model.McpToolParamType; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * MCP 工具方法参数注解:标注在 @McpToolClass 方法的参数上,以及 type=OBJECT 的 schemaClass 字段上。 + *

启动扫描时按注解属性组装 inputSchema;调用工具时按 fieldName 从入参取值绑定到方法实参。

+ * + * @author chenchuchuan + * @date 2026-08-31 + */ +@Documented +@Target({ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface McpToolParam { + + /** + * 入参字段名,即客户端传参时使用的 key。 + * 未配置时取 Java 参数名(默认编译带调试信息,可通过 DefaultParameterNameDiscoverer 获取)。 + */ + String fieldName() default ""; + + /** + * 字段类型(JSON Schema 的 type),默认字符串。 + */ + McpToolParamType type() default McpToolParamType.STRING; + + /** + * 字段描述,供大模型理解入参含义。 + */ + String description() default ""; + + /** + * 是否必填,默认 false。 + */ + boolean required() default false; + + /** + * type=OBJECT 时必填,指向的对象类型,其 @McpToolParam 标注字段会递归组装成嵌套 schema。 + */ + Class schemaClass() default Void.class; +} diff --git a/common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolConfig.java b/common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolConfig.java new file mode 100644 index 0000000..b6af926 --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolConfig.java @@ -0,0 +1,91 @@ +package apelet.common.core.mcp.config; + +import apelet.common.core.mcp.transport.StreamableHttpServerTransportProvider; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.server.McpServer; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.server.McpSyncServer; +import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.ServerResponse; + +import java.util.List; + +/** + * MCP Server 装配:把业务工具暴露为 MCP tool,供大模型(客户端)调用。 + * 基于 mcp-java8-sdk 0.8.1(构造器 API,非新版 builder())。 + * 该 SDK 不提供 listTools(),故注册时同步收集一份工具清单供调试接口读取(见 McpToolScanner.getTools())。 + * 双传输并存:SSE(/mcp/sse + /mcp/message)+ Streamable HTTP(/mcp 单端点)。 + * 工具由 McpToolScanner 启动时扫描 @McpToolClass 自动生成并统一注入。 + * + * @author chenchuchuan + * @date 2026-08-27 + */ +@Configuration +public class McpToolConfig { + + /** + * MCP 消息专用 ObjectMapper:应用注入的 ObjectMapper 在序列化 @JsonTypeInfo 多态对象 + * (如 CallToolResult.content 的 TextContent)时丢失 type 判别符,导致客户端反序列化失败, + * 故 MCP 的序列化统一走此普通 mapper(普通 mapper 会正常输出 "type":"text")。 + */ + private static final ObjectMapper MCP_OBJECT_MAPPER = new ObjectMapper(); + + @Autowired + private McpToolScanner mcpToolScanner; + + /** + * MCP 传输层(Spring MVC SSE):SSE 长连接端点 + JSON-RPC message 端点。 + * 端点路径带 /tenantadmin 前缀(本应用无 servlet context-path,模块 urlPrefix 统一用 /tenantadmin/xxx)。 + */ + @Bean + public WebMvcSseServerTransportProvider mcpTransport() { + return new WebMvcSseServerTransportProvider(MCP_OBJECT_MAPPER, "/tenantadmin/mcp/message", "/tenantadmin/mcp/sse"); + } + + /** + * 把 SSE transport 内部的 RouterFunction(SSE/message 两个端点)注册进 Spring MVC。 + */ + @Bean + public RouterFunction mcpEndpoints(WebMvcSseServerTransportProvider mcpTransport) { + return mcpTransport.getRouterFunction(); + } + + /** + * 构建同步 MCP Server(SSE 传输),并注册业务工具。 + */ + @Bean + public McpSyncServer mcpSyncServer(WebMvcSseServerTransportProvider mcpTransport) { + return registerTools(McpServer.sync(mcpTransport).serverInfo("business-mcp-server", "1.0.0")).build(); + } + + /** + * MCP 传输层(Streamable HTTP,单端点 POST /tenantadmin/mcp)。 + * 端点由 McpStreamableController 提供,不走 RouterFunction(避免应用转换器二次加工响应)。 + */ + @Bean + public StreamableHttpServerTransportProvider streamableHttpTransport() { + return new StreamableHttpServerTransportProvider(MCP_OBJECT_MAPPER); + } + + /** + * 构建同步 MCP Server(Streamable HTTP 传输),注册同一套业务工具。 + */ + @Bean + public McpSyncServer mcpStreamableSyncServer(StreamableHttpServerTransportProvider streamableHttpTransport) { + return registerTools(McpServer.sync(streamableHttpTransport).serverInfo("business-mcp-server", "1.0.0")).build(); + } + + /** + * 注册 MCP 工具(SSE 与 Streamable 两个 server 共用同一套注册逻辑)。 + * 工具清单由 McpToolScanner 扫描 @McpToolClass 自动组装,这里批量注入。 + */ + private McpServer.SyncSpecification registerTools(McpServer.SyncSpecification spec) { + List specifications = mcpToolScanner.getToolSpecifications(); + return specifications.isEmpty() ? spec : spec.tools(specifications); + } + +} diff --git a/common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolScanner.java b/common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolScanner.java new file mode 100644 index 0000000..f1a7edb --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolScanner.java @@ -0,0 +1,499 @@ +package apelet.common.core.mcp.config; + +import apelet.common.core.mcp.annotation.McpToolClass; +import apelet.common.core.mcp.annotation.McpToolParam; +import apelet.common.core.mcp.model.McpToolArgInfo; +import apelet.common.core.mcp.model.McpToolParamType; +import apelet.common.core.object.TokenData; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.spec.McpSchema; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.*; +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +/** + * MCP 工具扫描注册器:启动时从 Spring 容器取 @RestController/@Controller/@Component 等组件 bean, + * 扫描其上 @McpToolClass 标注的方法,自动组装 McpSchema.Tool 并生成调用 handler。 + *

直接基于容器 bean 扫描(getBeanNamesForAnnotation),不做 classpath 资源扫描, + * 避免 fat jar 嵌套 jar 里包名通配(**)扫不到的问题,也无需 scan-packages 等配置。 + * 组装结果以 SyncToolSpecification 列表暴露给 McpToolConfig 注入 SSE + Streamable 两个 server。

+ * + * @author chenchuchuan + * @date 2026-08-31 + */ +@Slf4j +@Component +public class McpToolScanner { + + /** + * 方法参数名发现器:优先 -parameters,兜底 LocalVariableTable(默认编译带调试信息)。 + */ + private static final DefaultParameterNameDiscoverer PARAM_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); + private final ApplicationContext applicationContext; + /** + * MCP 调用使用的默认用户Id(MCP 无 HTTP 登录态,服务内 TokenData.takeFromRequest() 会 NPE,调用前注入)。 + */ + @Value("${apelet.mcp.default-user-id:0}") + private Long defaultUserId; + /** + * 已组装好的工具注册项(供 spec.tools 批量注入)。 + */ + @Getter + private List toolSpecifications = Collections.emptyList(); + /** + * 已注册的工具清单(SDK 不提供 listTools(),供调试接口读取)。 + */ + private List toolList = Collections.emptyList(); + + public McpToolScanner(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + /** + * 按返回类型给动态代理一个安全的默认值(对象 null、字符串空、集合空、数组空、数值 0、布尔 false)。 + */ + private static Object defaultValue(Class returnType) { + if (returnType == String.class) { + return ""; + } + if (returnType == boolean.class) { + return false; + } + if (returnType == int.class) { + return 0; + } + if (returnType == long.class) { + return 0L; + } + if (returnType == List.class || returnType == Map.class || returnType == Set.class) { + return Collections.emptyMap(); + } + if (returnType.isArray()) { + return Array.newInstance(returnType.getComponentType(), 0); + } + return null; + } + + @PostConstruct + public void init() { + List specifications = new ArrayList<>(); + List tools = new ArrayList<>(); + // 直接从 Spring 容器取组件 bean 扫描,不做 classpath 资源扫描。 + // 原因:ClassPathScanningCandidateComponentProvider 走 PathMatchingResourcePatternResolver 递归匹配 classpath 资源, + // 应用打成 fat jar 后 @McpToolClass 类在 BOOT-INF/lib/*.jar 嵌套 jar 里,包名带 `**` 时匹配不上,导致扫不到; + // 改为从容器取 bean 则完全不受打包方式影响。@RestController/@Controller 都被 @Component 元注解标注, + // getBeanNamesForAnnotation(Component.class) 全部命中,@Service/@Repository 等组件同样覆盖。 + Set> scanned = new HashSet<>(); + for (String beanName : applicationContext.getBeanNamesForAnnotation(RestController.class)) { + Class clazz = applicationContext.getType(beanName); + if (clazz == null) { + continue; + } + clazz = ClassUtils.getUserClass(clazz); + if (!scanned.add(clazz)) { + continue; + } + this.scanClassMethods(specifications, tools, clazz, beanName); + } + this.toolSpecifications = specifications; + this.toolList = tools; + log.info("MCP 工具扫描完成,共注册 {} 个 tool:{}", tools.size(), + tools.stream().map(McpSchema.Tool::getName).collect(Collectors.toList())); + } + + /** + * 扫描单个 bean 类的 @McpToolClass 方法并注册。 + */ + private void scanClassMethods(List specifications, + List tools, Class clazz, String beanName) { + for (Method method : clazz.getDeclaredMethods()) { + // 只暴露 public 方法为 MCP 工具,私有/受保护方法不注册。 + if (!Modifier.isPublic(method.getModifiers())) { + continue; + } + McpToolClass ann = method.getAnnotation(McpToolClass.class); + if (ann == null) { + continue; + } + this.registerTool(specifications, tools, clazz, beanName, method, ann); + } + } + + public List getTools() { + return toolList; + } + + /** + * 组装单个工具:inputSchema + Tool + 调用 handler。 + */ + private void registerTool(List specifications, + List tools, Class clazz, String beanName, Method method, McpToolClass ann) { + List argInfos = this.buildArgInfos(method); + String inputSchema = this.buildInputSchema(argInfos); + McpSchema.Tool tool = new McpSchema.Tool(ann.name(), ann.description(), inputSchema); + // 懒取 bean:只有真正带 @McpToolClass 方法的类才触发初始化;拿到的是代理实例,invoke 时走 AOP。 + Object bean = applicationContext.getBean(beanName); + McpServerFeatures.SyncToolSpecification specification = new McpServerFeatures.SyncToolSpecification(); + specification.setTool(tool); + specification.setCall((exchange, args) -> this.invoke(bean, method, argInfos, args)); + specifications.add(specification); + tools.add(tool); + log.info("MCP 工具已注册:{} (inputSchema={})", tool.getName(), inputSchema); + } + + /** + * 组装方法参数绑定信息:@McpToolParam 未配置 fieldName 时取 Java 参数名。 + */ + private List buildArgInfos(Method method) { + Parameter[] parameters = method.getParameters(); + String[] paramNames = PARAM_NAME_DISCOVERER.getParameterNames(method); + List argInfos = new ArrayList<>(); + for (int i = 0; i < parameters.length; i++) { + McpToolParam ann = parameters[i].getAnnotation(McpToolParam.class); + if (ann == null) { + log.warn("MCP 工具方法 [{}#{}] 参数 [{}] 缺少 @McpToolParam,跳过该参数", method.getDeclaringClass().getSimpleName(), method.getName(), i); + continue; + } + String fieldName = StrUtil.isNotBlank(ann.fieldName()) ? ann.fieldName() + : (paramNames != null && paramNames.length > i ? paramNames[i] : "arg" + i); + argInfos.add(this.toArgInfo(i, parameters[i].getType(), parameters[i].getParameterizedType(), ann, fieldName)); + } + return argInfos; + } + + /** + * 由注解 + Java 类型组装元信息(方法参数与对象字段共用)。 + */ + private McpToolArgInfo toArgInfo(int paramIndex, Class paramType, Type genericType, McpToolParam ann, String fieldName) { + Class elementType = ann.type() == McpToolParamType.ARRAY ? this.inferElementType(genericType, paramType) : null; + Class schemaClass = ann.type() == McpToolParamType.OBJECT ? (ann.schemaClass() == Void.class ? paramType : ann.schemaClass()) : null; + return new McpToolArgInfo(paramIndex, fieldName, ann.type(), ann.description(), paramType, elementType, schemaClass, ann.required()); + } + + /** + * 组装 inputSchema:{"type":"object","properties":{...},"required":[...]}。 + */ + private String buildInputSchema(List argInfos) { + JSONObject root = new JSONObject(true); + root.put("type", "object"); + JSONObject properties = new JSONObject(true); + JSONArray required = new JSONArray(); + for (McpToolArgInfo info : argInfos) { + properties.put(info.getFieldName(), this.buildElementSchema(info)); + if (info.isRequired()) { + required.add(info.getFieldName()); + } + } + if (!properties.isEmpty()) { + root.put("properties", properties); + } + if (!required.isEmpty()) { + root.put("required", required); + } + return root.toJSONString(); + } + + /** + * 组装单个参数/字段的 JSON Schema;OBJECT 递归展开 schemaClass 的 @McpToolParam 字段。 + */ + private JSONObject buildElementSchema(McpToolArgInfo info) { + JSONObject schema = new JSONObject(true); + schema.put("type", info.getType().getJsonType()); + if (StrUtil.isNotBlank(info.getDescription())) { + schema.put("description", info.getDescription()); + } + if (info.getType() == McpToolParamType.OBJECT && info.getSchemaClass() != null) { + JSONObject properties = new JSONObject(true); + JSONArray required = new JSONArray(); + this.collectObjectSchema(info.getSchemaClass(), properties, required); + if (!properties.isEmpty()) { + schema.put("properties", properties); + } + if (!required.isEmpty()) { + schema.put("required", required); + } + } else if (info.getType() == McpToolParamType.ARRAY && info.getElementType() != null) { + JSONObject items = new JSONObject(true); + items.put("type", this.toJsonType(info.getElementType())); + schema.put("items", items); + } + return schema; + } + + /** + * 收集对象类型中 @McpToolParam 标注字段的嵌套 schema。 + */ + private void collectObjectSchema(Class schemaClass, JSONObject properties, JSONArray required) { + for (Field field : schemaClass.getDeclaredFields()) { + McpToolParam ann = field.getAnnotation(McpToolParam.class); + if (ann == null) { + continue; + } + String fieldName = StrUtil.isNotBlank(ann.fieldName()) ? ann.fieldName() : field.getName(); + properties.put(fieldName, this.buildElementSchema(this.toArgInfo(-1, field.getType(), field.getGenericType(), ann, fieldName))); + if (ann.required()) { + required.add(fieldName); + } + } + } + + /** + * 推断数组元素类型:List 泛型实参或数组组件类型,未知默认 String。 + */ + private Class inferElementType(Type genericType, Class rawType) { + if (genericType instanceof ParameterizedType) { + Type actual = ((ParameterizedType) genericType).getActualTypeArguments()[0]; + if (actual instanceof Class) { + return (Class) actual; + } + } + return rawType.isArray() ? rawType.getComponentType() : String.class; + } + + /** + * Java 类型 → JSON Schema type。 + */ + private String toJsonType(Class javaType) { + if (javaType == BigDecimal.class || javaType == Double.class || javaType == Float.class + || javaType == double.class || javaType == float.class) { + return "number"; + } + if (javaType == Long.class || javaType == Integer.class || javaType == Short.class || javaType == Byte.class + || javaType == long.class || javaType == int.class) { + return "integer"; + } + if (javaType == Boolean.class || javaType == boolean.class) { + return "boolean"; + } + return "string"; + } + + /** + * 调用工具方法:按元信息从入参 map 取参、转换类型、反射调用,结果包成 CallToolResult。 + *

MCP 调用运行在 reactor 调度线程,无 servlet 请求上下文,需先挂一个动态代理的 request/response + * (承载 TokenData)再调用,否则 @OperationLog 等切面在 RequestContextHolder 上强转 NPE。

+ */ + private McpSchema.CallToolResult invoke(Object bean, Method method, List argInfos, Map args) { + try { + //todo 这里要求带上token MCP 无 HTTP 登录态,注入默认用户 TokenData,避免服务内 takeFromRequest() 拿到 null 抛 NPE。 + TokenData tokenData = new TokenData(); + tokenData.setUserId(this.defaultUserId); + return this.withRequestContext(tokenData, () -> { + Object[] invokeArgs = this.bindArgs(args, argInfos, method.getParameterCount()); + Object result = method.invoke(bean, invokeArgs); + String text = result == null ? "" : (result instanceof String ? (String) result : JSON.toJSONString(result)); + return new McpSchema.CallToolResult(Collections.singletonList(new McpSchema.TextContent(text)), false); + }); + } catch (Exception e) { + Throwable cause = e instanceof InvocationTargetException ? ((InvocationTargetException) e).getTargetException() : e; + log.error("MCP 工具 [{}] 调用失败", method.getName(), cause); + return new McpSchema.CallToolResult(Collections.singletonList(new McpSchema.TextContent(cause.getMessage())), true); + } + } + + /** + * 在指定 TokenData 的请求上下文下执行动作:当前线程没有请求上下文时挂动态代理的 request/response, + * 执行完恢复原样。 + */ + private T withRequestContext(TokenData tokenData, ThrowingSupplier action) throws Exception { + RequestAttributes previous = RequestContextHolder.getRequestAttributes(); + boolean created = previous == null; + if (created) { + ServletRequestAttributes attributes = new ServletRequestAttributes(mcpMockRequest(), mcpMockResponse()); + RequestContextHolder.setRequestAttributes(attributes); + } + TokenData.addToRequest(tokenData); + try { + return action.get(); + } finally { + if (created) { + RequestContextHolder.resetRequestAttributes(); + } + } + } + + /** + * 动态代理的 HttpServletRequest:仅承载 getAttribute/setAttribute(供 TokenData),其余方法返回安全默认值。 + */ + private HttpServletRequest mcpMockRequest() { + return (HttpServletRequest) Proxy.newProxyInstance(this.getClass().getClassLoader(), + new Class[]{HttpServletRequest.class}, new MockRequestHandler()); + } + + /** + * 动态代理的 HttpServletResponse:setHeader 等方法空操作。 + */ + private HttpServletResponse mcpMockResponse() { + return (HttpServletResponse) Proxy.newProxyInstance(this.getClass().getClassLoader(), + new Class[]{HttpServletResponse.class}, (proxy, method, args1) -> defaultValue(method.getReturnType())); + } + + /** + * 按 argInfos 的 paramIndex 填充方法实参数组(数组长度按方法形参总数,未标注解的参数保持 null)。 + */ + private Object[] bindArgs(Map args, List argInfos, int parameterCount) { + Object[] invokeArgs = new Object[parameterCount]; + for (McpToolArgInfo info : argInfos) { + invokeArgs[info.getParamIndex()] = this.convertValue(args.get(info.getFieldName()), info); + } + return invokeArgs; + } + + /** + * 按元信息把入参 JSON 值转换为 Java 值(简单类型 / 数组 / 对象递归)。 + */ + private Object convertValue(Object raw, McpToolArgInfo info) { + if (raw == null) { + return null; + } + switch (info.getType()) { + case STRING: + return String.valueOf(raw); + case BOOLEAN: + return Boolean.valueOf(raw.toString()); + case INTEGER: + return this.toScalar(raw, info.getParamType()); + case NUMBER: + return this.toScalar(raw, info.getParamType()); + case ARRAY: + return this.toArray(raw, info.getParamType(), info.getElementType()); + case OBJECT: + return this.toObject(raw, info.getSchemaClass()); + default: + return raw; + } + } + + /** + * 构造数组实参:支持 List 与 E[],元素按元素类型转换。 + */ + private Object toArray(Object raw, Class paramType, Class elementType) { + List source = raw instanceof List ? (List) raw : Arrays.asList((Object[]) raw); + List converted = new ArrayList<>(); + for (Object item : source) { + converted.add(this.toScalar(item, elementType)); + } + if (!paramType.isArray()) { + return converted; + } + Object array = Array.newInstance(elementType, converted.size()); + for (int i = 0; i < converted.size(); i++) { + Array.set(array, i, converted.get(i)); + } + return array; + } + + /** + * 构造对象实参:反射 new schemaClass 实例,遍历 @McpToolParam 字段取值转换后 set 入。 + */ + private Object toObject(Object raw, Class schemaClass) { + if (!(raw instanceof Map) || schemaClass == null) { + return null; + } + Map map = (Map) raw; + try { + Object instance = schemaClass.getDeclaredConstructor().newInstance(); + for (Field field : schemaClass.getDeclaredFields()) { + McpToolParam ann = field.getAnnotation(McpToolParam.class); + if (ann == null) { + continue; + } + String fieldName = StrUtil.isNotBlank(ann.fieldName()) ? ann.fieldName() : field.getName(); + Object value = map.get(fieldName); + if (value == null) { + continue; + } + McpToolArgInfo fieldInfo = this.toArgInfo(-1, field.getType(), field.getGenericType(), ann, fieldName); + field.setAccessible(true); + field.set(instance, this.convertValue(value, fieldInfo)); + } + return instance; + } catch (Exception e) { + log.error("MCP 对象参数 [{}] 组装失败", schemaClass.getSimpleName(), e); + return null; + } + } + + /** + * 标量值按目标 Java 类型转换(String/数值/布尔)。 + */ + private Object toScalar(Object raw, Class type) { + if (type == String.class) { + return String.valueOf(raw); + } + if (type == Long.class || type == long.class) { + return Long.valueOf(raw.toString()); + } + if (type == Integer.class || type == int.class) { + return Integer.valueOf(raw.toString()); + } + if (type == BigDecimal.class) { + return new BigDecimal(raw.toString()); + } + if (type == Double.class || type == double.class) { + return Double.valueOf(raw.toString()); + } + if (type == Float.class || type == float.class) { + return Float.valueOf(raw.toString()); + } + if (type == Boolean.class || type == boolean.class) { + return Boolean.valueOf(raw.toString()); + } + return raw; + } + + /** + * 允许抛受检异常的 Supplier(method.invoke 抛 IllegalAccessException/InvocationTargetException)。 + */ + @FunctionalInterface + private interface ThrowingSupplier { + T get() throws Exception; + } + + /** + * 动态代理 request 处理器:getAttribute/setAttribute 落到内存 map,其余方法返回安全默认值。 + */ + private static class MockRequestHandler implements InvocationHandler { + + private final Map attributes = new HashMap<>(); + + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + String name = method.getName(); + if ("getAttribute".equals(name)) { + return attributes.get(args[0]); + } + if ("setAttribute".equals(name)) { + attributes.put((String) args[0], args[1]); + return null; + } + if ("getHeader".equals(name)) { + return attributes.get(args[0]); + } + if ("getRequestURI".equals(name)) { + return "/mcp"; + } + return defaultValue(method.getReturnType()); + } + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/mcp/controller/McpStreamableController.java b/common/common-core/src/main/java/apelet/common/core/mcp/controller/McpStreamableController.java new file mode 100644 index 0000000..08878ec --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/mcp/controller/McpStreamableController.java @@ -0,0 +1,47 @@ +package apelet.common.core.mcp.controller; + +import apelet.common.core.mcp.transport.StreamableHttpServerTransportProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * Streamable HTTP 单端点(POST /tenantadmin/mcp)。 + * 直接读写 HttpServletRequest/Response,序列化由 provider 完成并原样写入, + * 不经过 Spring 消息转换器(应用 ObjectMapper 会丢 @JsonTypeInfo 的 type 判别符 / 双重编码 JsonNode)。 + * + * @author chenchuchuan + * @date 2026-08-28 + */ +@RestController +@RequestMapping("/tenantadmin/mcp") +public class McpStreamableController { + + @Autowired + private StreamableHttpServerTransportProvider streamableHttpTransport; + + @PostMapping + public void handle(HttpServletRequest request, HttpServletResponse response, + @RequestHeader(value = "Mcp-Session-Id", required = false) String sessionHeader) throws IOException { + String body = new String(StreamUtils.copyToByteArray(request.getInputStream()), StandardCharsets.UTF_8); + StreamableHttpServerTransportProvider.McpProcessResult result = + streamableHttpTransport.processMcpRequest(body, sessionHeader); + response.setStatus(result.httpStatus); + response.setContentType("application/json;charset=UTF-8"); + if (result.sessionId != null) { + response.setHeader(StreamableHttpServerTransportProvider.SESSION_HEADER, result.sessionId); + } + String json = result.responseJson != null ? result.responseJson : result.errorJson; + if (json != null) { + response.getWriter().write(json); + } + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolArgInfo.java b/common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolArgInfo.java new file mode 100644 index 0000000..150d83b --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolArgInfo.java @@ -0,0 +1,71 @@ +package apelet.common.core.mcp.model; + +/** + * 单个 MCP 工具方法参数/对象字段的元信息:组装 inputSchema 与调用时绑定实参共用。 + * + * @author chenchuchuan + * @date 2026-08-31 + */ +public class McpToolArgInfo { + + /** 参数在方法形参列表中的下标;对象字段为 -1。 */ + private final int paramIndex; + /** 入参字段名(注解 fieldName 未配置时为 Java 参数名/属性名)。 */ + private final String fieldName; + /** 注解声明的类型(决定 inputSchema 的 type 与转换分支)。 */ + private final McpToolParamType type; + /** 字段描述。 */ + private final String description; + /** Java 类型(形参或字段类型)。 */ + private final Class paramType; + /** ARRAY 类型的元素类型(由泛型实参或数组组件类型推断),非 ARRAY 为 null。 */ + private final Class elementType; + /** OBJECT 类型指向的对象类型,非 OBJECT 为 null。 */ + private final Class schemaClass; + /** 是否必填(进 inputSchema 的 required 数组)。 */ + private final boolean required; + + public McpToolArgInfo(int paramIndex, String fieldName, McpToolParamType type, String description, + Class paramType, Class elementType, Class schemaClass, boolean required) { + this.paramIndex = paramIndex; + this.fieldName = fieldName; + this.type = type; + this.description = description; + this.paramType = paramType; + this.elementType = elementType; + this.schemaClass = schemaClass; + this.required = required; + } + + public int getParamIndex() { + return paramIndex; + } + + public String getFieldName() { + return fieldName; + } + + public McpToolParamType getType() { + return type; + } + + public String getDescription() { + return description; + } + + public Class getParamType() { + return paramType; + } + + public Class getElementType() { + return elementType; + } + + public Class getSchemaClass() { + return schemaClass; + } + + public boolean isRequired() { + return required; + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolParamType.java b/common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolParamType.java new file mode 100644 index 0000000..fe8be4e --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolParamType.java @@ -0,0 +1,35 @@ +package apelet.common.core.mcp.model; + +import lombok.Getter; + +/** + * MCP 参数类型枚举:value 即 JSON Schema 的 type 值。 + *

Java 侧实际的转换类型由方法参数声明决定,枚举只负责 inputSchema 的 type 与转换分支。

+ * + * @author chenchuchuan + * @date 2026-08-31 + */ +@Getter +public enum McpToolParamType { + + /** 字符串 */ + STRING("string"), + /** 数字(整数或浮点),对应 BigDecimal/Double/Float */ + NUMBER("number"), + /** 整数,对应 Integer/Long */ + INTEGER("integer"), + /** 布尔值 */ + BOOLEAN("boolean"), + /** 数组,元素类型由 List 泛型实参或数组组件类型推断 */ + ARRAY("array"), + /** 对象,需要配合 schemaClass 指定类型 */ + OBJECT("object"); + + /** JSON Schema 的 type 值 */ + private final String jsonType; + + McpToolParamType(String jsonType) { + this.jsonType = jsonType; + } + +} diff --git a/common/common-core/src/main/java/apelet/common/core/mcp/transport/StreamableHttpServerTransportProvider.java b/common/common-core/src/main/java/apelet/common/core/mcp/transport/StreamableHttpServerTransportProvider.java new file mode 100644 index 0000000..42af74a --- /dev/null +++ b/common/common-core/src/main/java/apelet/common/core/mcp/transport/StreamableHttpServerTransportProvider.java @@ -0,0 +1,230 @@ +package apelet.common.core.mcp.transport; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpServerSession; +import io.modelcontextprotocol.spec.McpServerTransport; +import io.modelcontextprotocol.spec.McpServerTransportProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * Streamable HTTP 传输层(MCP spec 2025-03-26 单端点 POST 传输)。 + * 基于 mcp-java8-sdk 0.8.1 的 SPI(McpServerTransportProvider + McpServerTransport)自研, + * 因该 fork 不提供 Streamable HTTP Provider(官方 SDK 0.9.0 才引入)。 + * 会话通过 Mcp-Session-Id 头管理:initialize 创建,后续请求携带。 + * 响应序列化由调用方(McpStreamableController)直接写入 HTTP 响应, + * 不经过 Spring 消息转换器,避免应用 ObjectMapper 丢失 @JsonTypeInfo 的 type 判别符 + * 或对 JsonNode 双重编码。 + * + * @author chenchuchuan + * @date 2026-08-28 + */ +public class StreamableHttpServerTransportProvider implements McpServerTransportProvider { + + private static final Logger logger = LoggerFactory.getLogger(StreamableHttpServerTransportProvider.class); + + public static final String SESSION_HEADER = "Mcp-Session-Id"; + + private final ObjectMapper objectMapper; + private final ConcurrentHashMap sessions = new ConcurrentHashMap<>(); + private final ConcurrentHashMap transports = new ConcurrentHashMap<>(); + private volatile boolean isClosing; + + private McpServerSession.Factory sessionFactory; + + public StreamableHttpServerTransportProvider(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public void setSessionFactory(McpServerSession.Factory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + @Override + public Mono notifyClients(String method, Map params) { + return Flux.fromIterable(sessions.values()) + .flatMap(session -> session.sendNotification(method, params)) + .then(); + } + + @Override + public Mono closeGracefully() { + isClosing = true; + return Flux.fromIterable(sessions.values()) + .flatMap(McpServerSession::closeGracefully) + .then(); + } + + /** + * 处理一条 MCP 请求:JSON-RPC 解析 + Mcp-Session-Id 会话路由 + 业务处理。 + * 返回序列化好的响应 JSON(含 type 判别符),由调用方原样写入 HTTP 响应。 + */ + public McpProcessResult processMcpRequest(String body, String sessionHeader) { + if (isClosing) { + return McpProcessResult.error(503, null, "Server is shutting down"); + } + McpSchema.JSONRPCMessage message; + try { + message = McpSchema.deserializeJsonRpcMessage(objectMapper, body); + } catch (Exception e) { + logger.error("Failed to deserialize MCP message: {}", e.getMessage()); + return McpProcessResult.error(400, null, "Invalid message format"); + } + + String sessionId = sessionHeader; + boolean isInitialize = message instanceof McpSchema.JSONRPCRequest + && "initialize".equals(((McpSchema.JSONRPCRequest) message).getMethod()); + + McpServerSession session; + StreamableHttpSessionTransport transport; + if (isInitialize && sessionId == null) { + sessionId = UUID.randomUUID().toString(); + transport = new StreamableHttpSessionTransport(sessionId, objectMapper); + session = sessionFactory.create(transport); + sessions.put(sessionId, session); + transports.put(sessionId, transport); + } else if (sessionId != null) { + transport = transports.get(sessionId); + session = sessions.get(sessionId); + if (session == null || transport == null) { + return McpProcessResult.error(404, sessionId, "Session not found: " + sessionId); + } + } else { + return McpProcessResult.error(400, null, "Session ID missing in message endpoint"); + } + + logger.info("MCP 请求 method={} sessionId={}", + message instanceof McpSchema.JSONRPCRequest ? ((McpSchema.JSONRPCRequest) message).getMethod() + : (message instanceof McpSchema.JSONRPCNotification ? ((McpSchema.JSONRPCNotification) message).getMethod() : "unknown"), + sessionId); + + try { + session.handle(message).block(Duration.ofSeconds(30)); + // 协议上客户端应在 initialize 后发送 notifications/initialized 才会进入 INITIALIZED 状态, + // 但直接 HTTP 调用(Postman / 平台扫描)往往省略该通知,导致后续 tools/list 等请求 + // 因 exchange 未就绪而在 exchangeSink.asMono() 上无限阻塞超时,故服务端自动补发。 + if (isInitialize) { + session.handle(new McpSchema.JSONRPCNotification("2.0", "notifications/initialized", Collections.emptyMap())) + .block(Duration.ofSeconds(30)); + } + } catch (Exception e) { + logger.error("Failed to process MCP message: {}", e.getMessage()); + return McpProcessResult.error(500, sessionId, "Failed to process MCP message: " + e.getMessage()); + } + + McpSchema.JSONRPCMessage response = null; + if (message instanceof McpSchema.JSONRPCRequest) { + try { + response = transport.pollResponse(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return McpProcessResult.error(500, sessionId, "Interrupted while waiting for response"); + } + } + try { + return new McpProcessResult(200, sessionId, response != null ? objectMapper.writeValueAsString(response) : null, null); + } catch (Exception e) { + logger.error("Failed to serialize MCP response: {}", e.getMessage()); + return McpProcessResult.error(500, sessionId, "Failed to serialize MCP response: " + e.getMessage()); + } + } + + /** + * MCP 请求处理结果,供控制器原样写入 HTTP 响应。 + */ + public static class McpProcessResult { + + /** HTTP 状态码 */ + public final int httpStatus; + /** 会话 id(可能为 null) */ + public final String sessionId; + /** 正常响应 JSON(含 type 判别符);通知类请求为 null */ + public final String responseJson; + /** 错误响应 JSON;正常时为 null */ + public final String errorJson; + + McpProcessResult(int httpStatus, String sessionId, String responseJson, String errorJson) { + this.httpStatus = httpStatus; + this.sessionId = sessionId; + this.responseJson = responseJson; + this.errorJson = errorJson; + } + + static McpProcessResult error(int httpStatus, String sessionId, String message) { + return new McpProcessResult(httpStatus, sessionId, null, + "{\"error\":\"" + message.replace("\"", "\\\"") + "\"}"); + } + } + + /** + * 每会话的传输层:sendMessage 把出站消息放入队列,由 processMcpRequest 取走序列化写回 HTTP 响应。 + */ + public static class StreamableHttpSessionTransport implements McpServerTransport { + + private final String sessionId; + private final ObjectMapper objectMapper; + private final BlockingQueue outboundMessages = new LinkedBlockingQueue<>(); + private volatile boolean isClosed; + + StreamableHttpSessionTransport(String sessionId, ObjectMapper objectMapper) { + this.sessionId = sessionId; + this.objectMapper = objectMapper; + } + + @Override + public Mono sendMessage(McpSchema.JSONRPCMessage message) { + return Mono.fromRunnable(() -> { + try { + if (isClosed) { + logger.warn("Failed to send message to session {}: transport is closed", sessionId); + return; + } + outboundMessages.offer(message); + logger.debug("Message sent to session {}", sessionId); + } catch (Exception e) { + logger.error("Failed to send message to session {}: {}", sessionId, e.getMessage()); + } + }); + } + + @Override + public T unmarshalFrom(Object data, TypeReference typeRef) { + return objectMapper.convertValue(data, typeRef); + } + + @Override + public Mono closeGracefully() { + return Mono.fromRunnable(() -> { + isClosed = true; + outboundMessages.clear(); + }); + } + + @Override + public void close() { + isClosed = true; + outboundMessages.clear(); + } + + /** + * 取出一条出站消息(通常是对当前请求的 JSON-RPC 响应)。 + */ + McpSchema.JSONRPCMessage pollResponse() throws InterruptedException { + return isClosed ? null : outboundMessages.poll(1, TimeUnit.SECONDS); + } + } +} diff --git a/common/common-core/src/main/java/apelet/common/core/object/MyPageParam.java b/common/common-core/src/main/java/apelet/common/core/object/MyPageParam.java index 3e78585..2028315 100644 --- a/common/common-core/src/main/java/apelet/common/core/object/MyPageParam.java +++ b/common/common-core/src/main/java/apelet/common/core/object/MyPageParam.java @@ -1,5 +1,7 @@ package apelet.common.core.object; +import apelet.common.core.mcp.annotation.McpToolParam; +import apelet.common.core.mcp.model.McpToolParamType; import lombok.Getter; /** @@ -18,16 +20,19 @@ public class MyPageParam { /** * 分页号码,从1开始计数。 */ + @McpToolParam(fieldName = "pageNum", type = McpToolParamType.INTEGER, description = "分页页号,从1开始") private Integer pageNum; /** * 每页大小。 */ + @McpToolParam(fieldName = "pageSize", type = McpToolParamType.INTEGER, description = "每页大小") private Integer pageSize; /** * 是否统计totalCount */ + @McpToolParam(fieldName = "count", type = McpToolParamType.BOOLEAN, description = "是否统计 totalCount", required = false) private Boolean count = true; /** diff --git a/common/common-online/src/main/java/apelet/common/online/controller/OnlineColumnController.java b/common/common-online/src/main/java/apelet/common/online/controller/OnlineColumnController.java index 42ade43..3ca5353 100644 --- a/common/common-online/src/main/java/apelet/common/online/controller/OnlineColumnController.java +++ b/common/common-online/src/main/java/apelet/common/online/controller/OnlineColumnController.java @@ -3,6 +3,9 @@ package apelet.common.online.controller; import apelet.common.core.annotation.MyRequestBody; import apelet.common.core.constant.ErrorCodeEnum; import apelet.common.core.constant.MaskFieldTypeEnum; +import apelet.common.core.mcp.annotation.McpToolClass; +import apelet.common.core.mcp.annotation.McpToolParam; +import apelet.common.core.mcp.model.McpToolParamType; import apelet.common.core.object.*; import apelet.common.core.util.MyCommonUtil; import apelet.common.core.util.MyModelUtil; @@ -176,9 +179,10 @@ public class OnlineColumnController { * @return 应答结果对象,包含查询结果集。 */ @PostMapping("/list") + @McpToolClass(name = "online_column_list", description = "分页查询在线表字段列表,pageParam 传 pageNum/pageSize") public ResponseResult> list( - @MyRequestBody OnlineColumnDto onlineColumnDtoFilter, - @MyRequestBody MyPageParam pageParam) { + @MyRequestBody @McpToolParam(fieldName = "onlineColumnDtoFilter", type = McpToolParamType.OBJECT, description = "字段过滤条件", schemaClass = OnlineColumnDto.class) OnlineColumnDto onlineColumnDtoFilter, + @MyRequestBody @McpToolParam(fieldName = "pageParam", type = McpToolParamType.OBJECT, description = "分页参数(pageNum/pageSize)", schemaClass = MyPageParam.class) MyPageParam pageParam) { if (pageParam != null) { PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); } diff --git a/common/common-online/src/main/java/apelet/common/online/controller/OnlineDatasourceRelationController.java b/common/common-online/src/main/java/apelet/common/online/controller/OnlineDatasourceRelationController.java index ee2c454..a405279 100644 --- a/common/common-online/src/main/java/apelet/common/online/controller/OnlineDatasourceRelationController.java +++ b/common/common-online/src/main/java/apelet/common/online/controller/OnlineDatasourceRelationController.java @@ -2,6 +2,9 @@ package apelet.common.online.controller; import apelet.common.core.annotation.MyRequestBody; import apelet.common.core.constant.ErrorCodeEnum; +import apelet.common.core.mcp.annotation.McpToolClass; +import apelet.common.core.mcp.annotation.McpToolParam; +import apelet.common.core.mcp.model.McpToolParamType; import apelet.common.core.object.*; import apelet.common.core.util.MyCommonUtil; import apelet.common.core.util.MyModelUtil; @@ -203,10 +206,11 @@ public class OnlineDatasourceRelationController { * @return 应答结果对象,包含查询结果集。 */ @PostMapping("/list") + @McpToolClass(name = "online_datasource_relation_list", description = "分页查询数据源关联列表,pageParam 传 pageNum/pageSize") public ResponseResult> list( - @MyRequestBody OnlineDatasourceRelationDto onlineDatasourceRelationDtoFilter, + @MyRequestBody @McpToolParam(fieldName = "onlineDatasourceRelationDtoFilter", type = McpToolParamType.OBJECT, description = "关联过滤条件", schemaClass = OnlineDatasourceRelationDto.class) OnlineDatasourceRelationDto onlineDatasourceRelationDtoFilter, @MyRequestBody MyOrderParam orderParam, - @MyRequestBody MyPageParam pageParam) { + @MyRequestBody @McpToolParam(fieldName = "pageParam", type = McpToolParamType.OBJECT, description = "分页参数(pageNum/pageSize)", schemaClass = MyPageParam.class) MyPageParam pageParam) { if (pageParam != null) { PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); } diff --git a/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java b/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java index f98c056..cbe1474 100644 --- a/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java +++ b/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java @@ -7,6 +7,9 @@ import apelet.common.core.cache.CacheKey; import apelet.common.core.constant.AppDeviceType; import apelet.common.core.constant.ErrorCodeEnum; import apelet.common.core.constant.GlobalDeletedFlag; +import apelet.common.core.mcp.annotation.McpToolClass; +import apelet.common.core.mcp.annotation.McpToolParam; +import apelet.common.core.mcp.model.McpToolParamType; import apelet.common.core.object.*; import apelet.common.core.util.MyCommonUtil; import apelet.common.core.util.MyModelUtil; @@ -149,7 +152,8 @@ public class OnlineFormController { @OperationLog(type = SysOperationLogType.ADD) @PostMapping("/add") @Transactional(rollbackFor = Exception.class) - public ResponseResult add(@MyRequestBody OnlineFormDto onlineFormDto) { + @McpToolClass(name = "online_form_add", description = "新增在线表单") + public ResponseResult add(@MyRequestBody @McpToolParam(fieldName = "onlineFormDto", type = McpToolParamType.OBJECT, description = "表单信息", schemaClass = OnlineFormDto.class, required = true) OnlineFormDto onlineFormDto) { // 快捷表单:走快捷表单保存逻辑 if (onlineFormDto.getFormType() != null && onlineFormDto.getFormType() == FormType.QUICK_FORM) { return ResponseResult.success(this.quickFormSave(onlineFormDto, null)); @@ -193,7 +197,8 @@ public class OnlineFormController { @OperationLog(type = SysOperationLogType.UPDATE) @PostMapping("/update") @Transactional(rollbackFor = Exception.class) - public ResponseResult update(@MyRequestBody OnlineFormDto onlineFormDto) { + @McpToolClass(name = "online_form_update", description = "修改在线表单") + public ResponseResult update(@MyRequestBody @McpToolParam(fieldName = "onlineFormDto", type = McpToolParamType.OBJECT, description = "表单信息", schemaClass = OnlineFormDto.class, required = true) OnlineFormDto onlineFormDto) { // 快捷表单:formType=QUICK_FORM 才是快捷表单,formId 即快捷表单id,走快捷表单保存逻辑 if (onlineFormDto.getFormType() != null && onlineFormDto.getFormType() == FormType.QUICK_FORM) { this.quickFormSave(onlineFormDto, onlineFormDto.getFormId()); @@ -302,7 +307,8 @@ public class OnlineFormController { */ @OperationLog(type = SysOperationLogType.DELETE) @PostMapping("/delete") - public ResponseResult delete(@MyRequestBody Long formId) { + @McpToolClass(name = "online_form_delete", description = "删除在线表单") + public ResponseResult delete(@MyRequestBody @McpToolParam(fieldName = "formId", type = McpToolParamType.INTEGER, description = "表单主键Id", required = true) Long formId) { String errorMessage; ResponseResult verifyResult = this.doVerifyAndGet(formId); if (!verifyResult.isSuccess()) { @@ -339,7 +345,8 @@ public class OnlineFormController { */ @OperationLog(type = SysOperationLogType.ADD) @PostMapping("/clone") - public ResponseResult clone(@MyRequestBody Long formId) { + @McpToolClass(name = "online_form_clone", description = "复制在线表单") + public ResponseResult clone(@MyRequestBody @McpToolParam(fieldName = "formId", type = McpToolParamType.INTEGER, description = "源表单主键Id", required = true) Long formId) { ResponseResult verifyResult = this.doVerifyAndGet(formId); if (!verifyResult.isSuccess()) { // formId 不是在线表单:可能是快捷表单,按快捷表单复制 @@ -406,7 +413,8 @@ public class OnlineFormController { * @return 应答结果对象,包含查询结果集。 */ @PostMapping("/list") - public ResponseResult> list(@MyRequestBody OnlineFormDto onlineFormDtoFilter, @MyRequestBody MyOrderParam orderParam, @MyRequestBody MyPageParam pageParam) { + @McpToolClass(name = "online_form_list", description = "分页查询在线表单,pageParam 传 pageNum/pageSize") + public ResponseResult> list(@MyRequestBody @McpToolParam(fieldName = "onlineFormDtoFilter", type = McpToolParamType.OBJECT, description = "过滤条件", schemaClass = OnlineFormDto.class) OnlineFormDto onlineFormDtoFilter, @MyRequestBody MyOrderParam orderParam, @MyRequestBody @McpToolParam(fieldName = "pageParam", type = McpToolParamType.OBJECT, description = "分页参数(pageNum/pageSize)", schemaClass = MyPageParam.class) MyPageParam pageParam) { // 不做分页:非快捷表单 + 快捷表单合并,再按前端排序字段排序 List voList = new ArrayList<>(); OnlineForm onlineFormFilter = MyModelUtil.copyTo(onlineFormDtoFilter, OnlineForm.class); @@ -432,7 +440,8 @@ public class OnlineFormController { * @return 应答结果对象,包含对象详情。 */ @GetMapping("/view") - public ResponseResult view(@RequestParam Long formId) { + @McpToolClass(name = "online_form_view", description = "查看在线表单详情") + public ResponseResult view(@RequestParam @McpToolParam(fieldName = "formId", type = McpToolParamType.INTEGER, description = "表单主键Id", required = true) Long formId) { ResponseResult verifyResult = this.doVerifyAndGet(formId); if (!verifyResult.isSuccess()) { return ResponseResult.errorFrom(verifyResult); diff --git a/common/common-online/src/main/java/apelet/common/online/controller/OnlinePageController.java b/common/common-online/src/main/java/apelet/common/online/controller/OnlinePageController.java index a6e37f9..47b6812 100644 --- a/common/common-online/src/main/java/apelet/common/online/controller/OnlinePageController.java +++ b/common/common-online/src/main/java/apelet/common/online/controller/OnlinePageController.java @@ -2,6 +2,9 @@ package apelet.common.online.controller; import apelet.common.core.annotation.MyRequestBody; import apelet.common.core.constant.ErrorCodeEnum; +import apelet.common.core.mcp.annotation.McpToolClass; +import apelet.common.core.mcp.annotation.McpToolParam; +import apelet.common.core.mcp.model.McpToolParamType; import apelet.common.core.object.*; import apelet.common.core.util.MyCommonUtil; import apelet.common.core.util.MyModelUtil; @@ -95,7 +98,8 @@ public class OnlinePageController { @OperationLog(type = SysOperationLogType.ADD) @Transactional(rollbackFor = Exception.class) @PostMapping("/add") - public ResponseResult add(@MyRequestBody OnlinePageDto onlinePageDto) { + @McpToolClass(name = "online_page_add", description = "新增在线表单页面数据,metaSource=new 时新建元数据,metaSource=existing 时使用已有表单元数据") + public ResponseResult add(@MyRequestBody @McpToolParam(fieldName = "onlinePageDto", type = McpToolParamType.OBJECT, description = "页面信息", schemaClass = OnlinePageDto.class, required = true) OnlinePageDto onlinePageDto) { String errorMessage = MyCommonUtil.getModelValidationError(onlinePageDto); if (errorMessage != null) { return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); @@ -404,7 +408,8 @@ public class OnlinePageController { */ @OperationLog(type = SysOperationLogType.UPDATE) @PostMapping("/update") - public ResponseResult update(@MyRequestBody OnlinePageDto onlinePageDto) { + @McpToolClass(name = "online_page_update", description = "修改在线表单页面") + public ResponseResult update(@MyRequestBody @McpToolParam(fieldName = "onlinePageDto", type = McpToolParamType.OBJECT, description = "页面信息", schemaClass = OnlinePageDto.class, required = true) OnlinePageDto onlinePageDto) { String errorMessage = MyCommonUtil.getModelValidationError(onlinePageDto, Default.class, UpdateGroup.class); try { if (errorMessage != null) { @@ -481,9 +486,10 @@ public class OnlinePageController { */ @OperationLog(type = SysOperationLogType.UPDATE) @PostMapping("/updatePublished") + @McpToolClass(name = "online_page_update_status", description = "发布/取消发布在线表单页面") public ResponseResult updateStatus( - @MyRequestBody(required = true) Long pageId, - @MyRequestBody(required = true) Boolean published) { + @MyRequestBody(required = true) @McpToolParam(fieldName = "pageId", type = McpToolParamType.INTEGER, description = "页面主键Id", required = true) Long pageId, + @MyRequestBody(required = true) @McpToolParam(fieldName = "published", type = McpToolParamType.BOOLEAN, description = "发布状态:true=发布,false=取消发布", required = true) Boolean published) { String errorMessage; ResponseResult verifyResult = this.doVerifyAndGet(pageId); if (!verifyResult.isSuccess()) { @@ -513,7 +519,8 @@ public class OnlinePageController { */ @OperationLog(type = SysOperationLogType.DELETE) @PostMapping("/delete") - public ResponseResult delete(@MyRequestBody Long pageId) { + @McpToolClass(name = "online_page_delete", description = "删除在线表单页面") + public ResponseResult delete(@MyRequestBody @McpToolParam(fieldName = "pageId", type = McpToolParamType.INTEGER, description = "页面主键Id", required = true) Long pageId) { String errorMessage; ResponseResult verifyResult = this.doVerifyAndGet(pageId); if (!verifyResult.isSuccess()) { @@ -538,10 +545,11 @@ public class OnlinePageController { * @return 应答结果对象,包含查询结果集。 */ @PostMapping("/list") + @McpToolClass(name = "online_page_list", description = "分页查询在线表单页面,pageParam 传 pageNum/pageSize") public ResponseResult> list( - @MyRequestBody OnlinePageDto onlinePageDtoFilter, + @MyRequestBody @McpToolParam(fieldName = "onlinePageDtoFilter", type = McpToolParamType.OBJECT, description = "过滤条件", schemaClass = OnlinePageDto.class) OnlinePageDto onlinePageDtoFilter, @MyRequestBody MyOrderParam orderParam, - @MyRequestBody MyPageParam pageParam) { + @MyRequestBody @McpToolParam(fieldName = "pageParam", type = McpToolParamType.OBJECT, description = "分页参数(pageNum/pageSize)", schemaClass = MyPageParam.class) MyPageParam pageParam) { if (pageParam != null) { PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); } @@ -574,7 +582,8 @@ public class OnlinePageController { * @return 应答结果对象,包含对象详情。 */ @GetMapping("/view") - public ResponseResult view(@RequestParam Long pageId) { + @McpToolClass(name = "online_page_view", description = "查看在线表单页面详情") + public ResponseResult view(@RequestParam @McpToolParam(fieldName = "pageId", type = McpToolParamType.INTEGER, description = "页面主键Id", required = true) Long pageId) { ResponseResult verifyResult = this.doVerifyAndGet(pageId); if (!verifyResult.isSuccess()) { return ResponseResult.errorFrom(verifyResult); @@ -597,11 +606,12 @@ public class OnlinePageController { * @return 应答结果对象,返回符合条件的数据列表。 */ @PostMapping("/listOnlinePageDatasource") + @McpToolClass(name = "online_page_list_datasource", description = "分页查询在线表单页面关联的数据源") public ResponseResult> listOnlinePageDatasource( - @MyRequestBody Long pageId, - @MyRequestBody OnlineDatasourceDto onlineDatasourceDtoFilter, + @MyRequestBody @McpToolParam(fieldName = "pageId", type = McpToolParamType.INTEGER, description = "页面主键Id", required = true) Long pageId, + @MyRequestBody @McpToolParam(fieldName = "onlineDatasourceDtoFilter", type = McpToolParamType.OBJECT, description = "数据源过滤条件", schemaClass = OnlineDatasourceDto.class) OnlineDatasourceDto onlineDatasourceDtoFilter, @MyRequestBody MyOrderParam orderParam, - @MyRequestBody MyPageParam pageParam) { + @MyRequestBody @McpToolParam(fieldName = "pageParam", type = McpToolParamType.OBJECT, description = "分页参数(pageNum/pageSize)", schemaClass = MyPageParam.class) MyPageParam pageParam) { ResponseResult verifyResult = this.doVerifyAndGet(pageId); if (!verifyResult.isSuccess()) { return ResponseResult.errorFrom(verifyResult); diff --git a/common/common-online/src/main/java/apelet/common/online/dto/OnlineColumnDto.java b/common/common-online/src/main/java/apelet/common/online/dto/OnlineColumnDto.java index 295644f..6fed546 100644 --- a/common/common-online/src/main/java/apelet/common/online/dto/OnlineColumnDto.java +++ b/common/common-online/src/main/java/apelet/common/online/dto/OnlineColumnDto.java @@ -1,5 +1,7 @@ package apelet.common.online.dto; +import apelet.common.core.mcp.annotation.McpToolParam; +import apelet.common.core.mcp.model.McpToolParamType; import apelet.common.core.validator.ConstDictRef; import apelet.common.core.validator.UpdateGroup; import apelet.common.online.model.constant.FieldFilterType; @@ -25,6 +27,7 @@ public class OnlineColumnDto { */ @Schema(description = "主键Id") @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + @McpToolParam(fieldName = "columnId", type = McpToolParamType.INTEGER, description = "字段主键Id") private Long columnId; /** @@ -32,6 +35,7 @@ public class OnlineColumnDto { */ @Schema(description = "字段名") @NotBlank(message = "数据验证失败,字段名不能为空!") + @McpToolParam(fieldName = "columnName", type = McpToolParamType.STRING, description = "字段名") private String columnName; /** @@ -39,6 +43,7 @@ public class OnlineColumnDto { */ @Schema(description = "数据表Id") @NotNull(message = "数据验证失败,数据表Id不能为空!") + @McpToolParam(fieldName = "tableId", type = McpToolParamType.INTEGER, description = "数据表Id") private Long tableId; /** @@ -46,6 +51,7 @@ public class OnlineColumnDto { */ @Schema(description = "数据表中的字段类型") @NotBlank(message = "数据验证失败,数据表中的字段类型不能为空!") + @McpToolParam(fieldName = "columnType", type = McpToolParamType.STRING, description = "数据表中的字段类型") private String columnType; /** @@ -53,6 +59,7 @@ public class OnlineColumnDto { */ @Schema(description = "数据表中的完整字段类型") @NotBlank(message = "数据验证失败,数据表中的完整字段类型(包括了精度和刻度)不能为空!") + @McpToolParam(fieldName = "fullColumnType", type = McpToolParamType.STRING, description = "完整字段类型(含精度刻度)") private String fullColumnType; /** @@ -60,6 +67,7 @@ public class OnlineColumnDto { */ @Schema(description = "是否为主键") @NotNull(message = "数据验证失败,是否为主键不能为空!") + @McpToolParam(fieldName = "primaryKey", type = McpToolParamType.BOOLEAN, description = "是否为主键") private Boolean primaryKey; /** @@ -67,6 +75,7 @@ public class OnlineColumnDto { */ @Schema(description = "是否是自增主键") @NotNull(message = "数据验证失败,是否是自增主键(0: 不是 1: 是)不能为空!") + @McpToolParam(fieldName = "autoIncrement", type = McpToolParamType.BOOLEAN, description = "是否自增主键") private Boolean autoIncrement; /** @@ -74,12 +83,14 @@ public class OnlineColumnDto { */ @Schema(description = "是否可以为空") @NotNull(message = "数据验证失败,是否可以为空 (0: 不可以为空 1: 可以为空)不能为空!") + @McpToolParam(fieldName = "nullable", type = McpToolParamType.BOOLEAN, description = "是否可以为空") private Boolean nullable; /** * 缺省值。 */ @Schema(description = "缺省值") + @McpToolParam(fieldName = "columnDefault", type = McpToolParamType.STRING, description = "缺省值") private String columnDefault; /** @@ -87,12 +98,14 @@ public class OnlineColumnDto { */ @Schema(description = "字段在数据表中的显示位置") @NotNull(message = "数据验证失败,字段在数据表中的显示位置不能为空!") + @McpToolParam(fieldName = "columnShowOrder", type = McpToolParamType.INTEGER, description = "字段显示位置") private Integer columnShowOrder; /** * 数据表中的字段注释。 */ @Schema(description = "数据表中的字段注释") + @McpToolParam(fieldName = "columnComment", type = McpToolParamType.STRING, description = "字段注释") private String columnComment; /** @@ -100,6 +113,7 @@ public class OnlineColumnDto { */ @Schema(description = "对象映射字段名称") @NotBlank(message = "数据验证失败,对象映射字段名称不能为空!") + @McpToolParam(fieldName = "objectFieldName", type = McpToolParamType.STRING, description = "对象映射字段名称") private String objectFieldName; /** @@ -107,18 +121,21 @@ public class OnlineColumnDto { */ @Schema(description = "对象映射字段类型") @NotBlank(message = "数据验证失败,对象映射字段类型不能为空!") + @McpToolParam(fieldName = "objectFieldType", type = McpToolParamType.STRING, description = "对象映射字段类型") private String objectFieldType; /** * 数值型字段的精度(目前仅Oracle使用)。 */ @Schema(description = "数值型字段的精度") + @McpToolParam(fieldName = "numericPrecision", type = McpToolParamType.INTEGER, description = "数值精度") private Integer numericPrecision; /** * 数值型字段的刻度(小数点后位数,目前仅Oracle使用)。 */ @Schema(description = "数值型字段的刻度") + @McpToolParam(fieldName = "numericScale", type = McpToolParamType.INTEGER, description = "数值刻度") private Integer numericScale; /** @@ -127,6 +144,7 @@ public class OnlineColumnDto { @Schema(description = "过滤类型字段") @NotNull(message = "数据验证失败,过滤类型字段不能为空!", groups = {UpdateGroup.class}) @ConstDictRef(constDictClass = FieldFilterType.class, message = "数据验证失败,过滤类型字段为无效值!") + @McpToolParam(fieldName = "filterType", type = McpToolParamType.INTEGER, description = "过滤类型") private Integer filterType; /** @@ -134,6 +152,7 @@ public class OnlineColumnDto { */ @Schema(description = "是否是主键的父Id") @NotNull(message = "数据验证失败,是否是主键的父Id不能为空!") + @McpToolParam(fieldName = "parentKey", type = McpToolParamType.BOOLEAN, description = "是否主键的父Id") private Boolean parentKey; /** @@ -141,6 +160,7 @@ public class OnlineColumnDto { */ @Schema(description = "是否部门过滤字段") @NotNull(message = "数据验证失败,是否部门过滤字段标记不能为空!") + @McpToolParam(fieldName = "deptFilter", type = McpToolParamType.BOOLEAN, description = "是否部门过滤字段") private Boolean deptFilter; /** @@ -148,6 +168,7 @@ public class OnlineColumnDto { */ @Schema(description = "是否用户过滤字段") @NotNull(message = "数据验证失败,是否用户过滤字段标记不能为空!") + @McpToolParam(fieldName = "userFilter", type = McpToolParamType.BOOLEAN, description = "是否用户过滤字段") private Boolean userFilter; /** @@ -155,35 +176,41 @@ public class OnlineColumnDto { */ @Schema(description = "字段类别") @ConstDictRef(constDictClass = FieldKind.class, message = "数据验证失败,字段类别为无效值!") + @McpToolParam(fieldName = "fieldKind", type = McpToolParamType.INTEGER, description = "字段类别") private Integer fieldKind; /** * 包含的文件文件数量,0表示无限制。 */ @Schema(description = "包含的文件文件数量,0表示无限制") + @McpToolParam(fieldName = "maxFileCount", type = McpToolParamType.INTEGER, description = "包含的文件数量,0表示无限制") private Integer maxFileCount; /** * 上传文件系统类型。 */ @Schema(description = "上传文件系统类型") + @McpToolParam(fieldName = "uploadFileSystemType", type = McpToolParamType.INTEGER, description = "上传文件系统类型") private Integer uploadFileSystemType; /** * 脱敏字段类型,具体值可参考MaskFieldTypeEnum枚举。 */ @Schema(description = "脱敏字段类型") + @McpToolParam(fieldName = "maskFieldType", type = McpToolParamType.STRING, description = "脱敏字段类型") private String maskFieldType; /** * 编码规则的JSON格式数据。 */ @Schema(description = "编码规则的JSON格式数据") + @McpToolParam(fieldName = "encodedRule", type = McpToolParamType.STRING, description = "编码规则JSON") private String encodedRule; /** * 字典Id。 */ @Schema(description = "字典Id") + @McpToolParam(fieldName = "dictId", type = McpToolParamType.INTEGER, description = "字典Id") private Long dictId; } diff --git a/common/common-online/src/main/java/apelet/common/online/dto/OnlineDatasourceDto.java b/common/common-online/src/main/java/apelet/common/online/dto/OnlineDatasourceDto.java index 0df6347..1f4cdcc 100644 --- a/common/common-online/src/main/java/apelet/common/online/dto/OnlineDatasourceDto.java +++ b/common/common-online/src/main/java/apelet/common/online/dto/OnlineDatasourceDto.java @@ -1,5 +1,7 @@ package apelet.common.online.dto; +import apelet.common.core.mcp.annotation.McpToolParam; +import apelet.common.core.mcp.model.McpToolParamType; import apelet.common.core.validator.AddGroup; import apelet.common.core.validator.UpdateGroup; import io.swagger.v3.oas.annotations.media.Schema; @@ -23,6 +25,7 @@ public class OnlineDatasourceDto { */ @Schema(description = "主键Id") @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + @McpToolParam(fieldName = "datasourceId", type = McpToolParamType.INTEGER, description = "数据源主键Id") private Long datasourceId; /** @@ -30,6 +33,7 @@ public class OnlineDatasourceDto { */ @Schema(description = "数据源名称") @NotBlank(message = "数据验证失败,数据源名称不能为空!") + @McpToolParam(fieldName = "datasourceName", type = McpToolParamType.STRING, description = "数据源名称") private String datasourceName; /** @@ -37,6 +41,7 @@ public class OnlineDatasourceDto { */ @Schema(description = "数据源变量名,会成为数据访问url的一部分") @NotBlank(message = "数据验证失败,数据源变量名不能为空!") + @McpToolParam(fieldName = "variableName", type = McpToolParamType.STRING, description = "数据源变量名") private String variableName; /** @@ -44,6 +49,7 @@ public class OnlineDatasourceDto { */ @Schema(description = "主表所在的数据库链接Id") @NotNull(message = "数据验证失败,数据库链接Id不能为空!") + @McpToolParam(fieldName = "dblinkId", type = McpToolParamType.INTEGER, description = "数据库链接Id") private Long dblinkId; /** @@ -51,6 +57,7 @@ public class OnlineDatasourceDto { */ @Schema(description = "主表Id") @NotNull(message = "数据验证失败,主表Id不能为空!", groups = {UpdateGroup.class}) + @McpToolParam(fieldName = "masterTableId", type = McpToolParamType.INTEGER, description = "主表Id") private Long masterTableId; /** @@ -58,5 +65,6 @@ public class OnlineDatasourceDto { */ @Schema(description = "主表表名") @NotBlank(message = "数据验证失败,主表名不能为空!", groups = {AddGroup.class}) + @McpToolParam(fieldName = "masterTableName", type = McpToolParamType.STRING, description = "主表表名") private String masterTableName; } diff --git a/common/common-online/src/main/java/apelet/common/online/dto/OnlineDatasourceRelationDto.java b/common/common-online/src/main/java/apelet/common/online/dto/OnlineDatasourceRelationDto.java index 5c49c7f..c98f146 100644 --- a/common/common-online/src/main/java/apelet/common/online/dto/OnlineDatasourceRelationDto.java +++ b/common/common-online/src/main/java/apelet/common/online/dto/OnlineDatasourceRelationDto.java @@ -1,5 +1,7 @@ package apelet.common.online.dto; +import apelet.common.core.mcp.annotation.McpToolParam; +import apelet.common.core.mcp.model.McpToolParamType; import apelet.common.core.validator.AddGroup; import apelet.common.core.validator.ConstDictRef; import apelet.common.core.validator.UpdateGroup; @@ -25,6 +27,7 @@ public class OnlineDatasourceRelationDto { */ @Schema(description = "主键Id") @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + @McpToolParam(fieldName = "relationId", type = McpToolParamType.INTEGER, description = "关联主键Id") private Long relationId; /** @@ -32,6 +35,7 @@ public class OnlineDatasourceRelationDto { */ @Schema(description = "关联名称") @NotBlank(message = "数据验证失败,关联名称不能为空!") + @McpToolParam(fieldName = "relationName", type = McpToolParamType.STRING, description = "关联名称") private String relationName; /** @@ -39,6 +43,7 @@ public class OnlineDatasourceRelationDto { */ @Schema(description = "变量名") @NotBlank(message = "数据验证失败,变量名不能为空!") + @McpToolParam(fieldName = "variableName", type = McpToolParamType.STRING, description = "变量名") private String variableName; /** @@ -46,6 +51,7 @@ public class OnlineDatasourceRelationDto { */ @Schema(description = "主数据源Id") @NotNull(message = "数据验证失败,主数据源Id不能为空!") + @McpToolParam(fieldName = "datasourceId", type = McpToolParamType.INTEGER, description = "主数据源Id") private Long datasourceId; /** @@ -54,6 +60,7 @@ public class OnlineDatasourceRelationDto { @Schema(description = "关联类型") @NotNull(message = "数据验证失败,关联类型不能为空!") @ConstDictRef(constDictClass = RelationType.class, message = "数据验证失败,关联类型为无效值!") + @McpToolParam(fieldName = "relationType", type = McpToolParamType.INTEGER, description = "关联类型(0=一对一,1=一对多)") private Integer relationType; /** @@ -61,6 +68,7 @@ public class OnlineDatasourceRelationDto { */ @Schema(description = "主表关联字段Id") @NotNull(message = "数据验证失败,主表关联字段Id不能为空!") + @McpToolParam(fieldName = "masterColumnId", type = McpToolParamType.INTEGER, description = "主表关联字段Id") private Long masterColumnId; /** @@ -68,6 +76,7 @@ public class OnlineDatasourceRelationDto { */ @Schema(description = "从表Id") @NotNull(message = "数据验证失败,从表Id不能为空!", groups = {UpdateGroup.class}) + @McpToolParam(fieldName = "slaveTableId", type = McpToolParamType.INTEGER, description = "从表Id") private Long slaveTableId; /** @@ -75,6 +84,7 @@ public class OnlineDatasourceRelationDto { */ @Schema(description = "从表名") @NotBlank(message = "数据验证失败,从表名不能为空!", groups = {AddGroup.class}) + @McpToolParam(fieldName = "slaveTableName", type = McpToolParamType.STRING, description = "从表名") private String slaveTableName; /** @@ -82,6 +92,7 @@ public class OnlineDatasourceRelationDto { */ @Schema(description = "从表关联字段Id") @NotNull(message = "数据验证失败,从表关联字段Id不能为空!", groups = {UpdateGroup.class}) + @McpToolParam(fieldName = "slaveColumnId", type = McpToolParamType.INTEGER, description = "从表关联字段Id") private Long slaveColumnId; /** @@ -89,6 +100,7 @@ public class OnlineDatasourceRelationDto { */ @Schema(description = "从表字段名") @NotBlank(message = "数据验证失败,从表字段名不能为空!", groups = {AddGroup.class}) + @McpToolParam(fieldName = "slaveColumnName", type = McpToolParamType.STRING, description = "从表字段名") private String slaveColumnName; /** @@ -96,6 +108,7 @@ public class OnlineDatasourceRelationDto { */ @Schema(description = "是否级联删除标记") @NotNull(message = "数据验证失败,是否级联删除标记不能为空!") + @McpToolParam(fieldName = "cascadeDelete", type = McpToolParamType.BOOLEAN, description = "是否级联删除") private Boolean cascadeDelete; /** @@ -103,5 +116,6 @@ public class OnlineDatasourceRelationDto { */ @Schema(description = "是否左连接标记") @NotNull(message = "数据验证失败,是否左连接标记不能为空!") + @McpToolParam(fieldName = "leftJoin", type = McpToolParamType.BOOLEAN, description = "是否左连接") private Boolean leftJoin; } diff --git a/common/common-online/src/main/java/apelet/common/online/dto/OnlineFormDto.java b/common/common-online/src/main/java/apelet/common/online/dto/OnlineFormDto.java index 8fb49b7..1ed1a4a 100644 --- a/common/common-online/src/main/java/apelet/common/online/dto/OnlineFormDto.java +++ b/common/common-online/src/main/java/apelet/common/online/dto/OnlineFormDto.java @@ -1,5 +1,7 @@ package apelet.common.online.dto; +import apelet.common.core.mcp.annotation.McpToolParam; +import apelet.common.core.mcp.model.McpToolParamType; import apelet.common.core.validator.ConstDictRef; import apelet.common.core.validator.UpdateGroup; import apelet.common.online.model.OnlineQuickFormField; @@ -27,6 +29,7 @@ public class OnlineFormDto { */ @Schema(description = "主键Id") @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + @McpToolParam(fieldName = "formId", type = McpToolParamType.INTEGER, description = "主键Id(编辑时使用)") private Long formId; /** @@ -34,12 +37,14 @@ public class OnlineFormDto { */ @Schema(description = "页面id") @NotNull(message = "数据验证失败,页面id不能为空!") + @McpToolParam(fieldName = "pageId", type = McpToolParamType.INTEGER, description = "页面id", required = true) private Long pageId; /** * 表单编码。 */ @Schema(description = "表单编码") + @McpToolParam(fieldName = "formCode", type = McpToolParamType.STRING, description = "表单编码") private String formCode; /** @@ -47,6 +52,7 @@ public class OnlineFormDto { */ @Schema(description = "表单名称") @NotBlank(message = "数据验证失败,表单名称不能为空!") + @McpToolParam(fieldName = "formName", type = McpToolParamType.STRING, description = "表单名称", required = true) private String formName; /** @@ -55,6 +61,7 @@ public class OnlineFormDto { @Schema(description = "表单类别") @NotNull(message = "数据验证失败,表单类别不能为空!") @ConstDictRef(constDictClass = FormKind.class, message = "数据验证失败,表单类别为无效值!") + @McpToolParam(fieldName = "formKind", type = McpToolParamType.INTEGER, description = "表单类别") private Integer formKind; /** @@ -63,6 +70,7 @@ public class OnlineFormDto { @Schema(description = "表单类型") @NotNull(message = "数据验证失败,表单类型不能为空!") @ConstDictRef(constDictClass = FormType.class, message = "数据验证失败,表单类型为无效值!") + @McpToolParam(fieldName = "formType", type = McpToolParamType.INTEGER, description = "表单类型", required = true) private Integer formType; /** @@ -70,36 +78,42 @@ public class OnlineFormDto { */ @Schema(description = "表单主表id") @NotNull(message = "数据验证失败,表单主表id不能为空!") + @McpToolParam(fieldName = "masterTableId", type = McpToolParamType.INTEGER, description = "表单主表id", required = true) private Long masterTableId; /** * 当前表单关联的数据源Id集合。 */ @Schema(description = "当前表单关联的数据源Id集合") + @McpToolParam(fieldName = "datasourceIdList", type = McpToolParamType.ARRAY, description = "当前表单关联的数据源Id集合") private List datasourceIdList; /** * 表单组件JSON。 */ @Schema(description = "表单组件JSON") + @McpToolParam(fieldName = "widgetJson", type = McpToolParamType.STRING, description = "表单组件JSON") private String widgetJson; /** * 表单参数JSON。 */ @Schema(description = "表单参数JSON") + @McpToolParam(fieldName = "paramsJson", type = McpToolParamType.STRING, description = "表单参数JSON") private String paramsJson; /** * 快捷表单ID(编辑快捷表单配置时回传)。 */ @Schema(description = "快捷表单ID") + @McpToolParam(fieldName = "quickFormId", type = McpToolParamType.INTEGER, description = "快捷表单ID") private Long quickFormId; /** * 排版类型(快捷表单)。 */ @Schema(description = "排版类型") + @McpToolParam(fieldName = "layoutType", type = McpToolParamType.INTEGER, description = "排版类型(快捷表单)") private Integer layoutType; /** diff --git a/common/common-online/src/main/java/apelet/common/online/dto/OnlinePageDto.java b/common/common-online/src/main/java/apelet/common/online/dto/OnlinePageDto.java index afa6f86..2e29f8e 100644 --- a/common/common-online/src/main/java/apelet/common/online/dto/OnlinePageDto.java +++ b/common/common-online/src/main/java/apelet/common/online/dto/OnlinePageDto.java @@ -1,5 +1,7 @@ package apelet.common.online.dto; +import apelet.common.core.mcp.annotation.McpToolParam; +import apelet.common.core.mcp.model.McpToolParamType; import apelet.common.core.validator.ConstDictRef; import apelet.common.core.validator.UpdateGroup; import apelet.common.online.model.constant.PageStatus; @@ -25,12 +27,14 @@ public class OnlinePageDto { */ @Schema(description = "主键Id") @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class}) + @McpToolParam(fieldName = "pageId", type = McpToolParamType.INTEGER, description = "主键Id(编辑时使用)") private Long pageId; /** * 页面编码。 */ @Schema(description = "页面编码") + @McpToolParam(fieldName = "pageCode", type = McpToolParamType.STRING, description = "页面编码(metaSource=new 时必填)") private String pageCode; /** @@ -38,6 +42,7 @@ public class OnlinePageDto { */ @Schema(description = "页面名称") @NotBlank(message = "数据验证失败,页面名称不能为空!") + @McpToolParam(fieldName = "pageName", type = McpToolParamType.STRING, description = "页面名称", required = true) private String pageName; /** @@ -46,6 +51,7 @@ public class OnlinePageDto { @Schema(description = "页面类型") @NotNull(message = "数据验证失败,页面类型不能为空!") @ConstDictRef(constDictClass = PageType.class, message = "数据验证失败,页面类型为无效值!") + @McpToolParam(fieldName = "pageType", type = McpToolParamType.INTEGER, description = "页面类型", required = true) private Integer pageType; /** @@ -54,29 +60,34 @@ public class OnlinePageDto { @Schema(description = "页面编辑状态") @NotNull(message = "数据验证失败,状态不能为空!") @ConstDictRef(constDictClass = PageStatus.class, message = "数据验证失败,状态为无效值!") + @McpToolParam(fieldName = "status", type = McpToolParamType.INTEGER, description = "页面编辑状态", required = true) private Integer status; /** * 表单元数据。 */ @Schema(description = "表单元数据") + @McpToolParam(fieldName = "masterTableId", type = McpToolParamType.STRING, description = "表单元数据Id(metaSource=existing 时必填)") private String masterTableId; /** * 表单元数据。 */ @Schema(description = "表单元数据表名") + @McpToolParam(fieldName = "masterTableName", type = McpToolParamType.STRING, description = "表单元数据表名") private String masterTableName; /** * 数据来源: "new"=新建元数据, "existing"=使用已有元数据。 */ @Schema(description = "数据来源: new=新建元数据, existing=使用已有元数据") + @McpToolParam(fieldName = "metaSource", type = McpToolParamType.STRING, description = "数据来源:new=新建元数据,existing=使用已有元数据", required = true) private String metaSource; /** * 表类型: "2"=基础资料(单表), "1"=业务单据(主表+附表)。仅在metaSource="new"时有效。 */ @Schema(description = "表类型: 2=基础资料(单表), 1=业务单据(主表+附表)。仅在metaSource=new时有效") + @McpToolParam(fieldName = "metaType", type = McpToolParamType.STRING, description = "表类型:2=基础资料(单表),1=业务单据(主表+附表),仅在 metaSource=new 时有效") private String metaType; }