diff --git a/solon-ai-in-quarkus/README.md b/solon-ai-in-quarkus/README.md
index 7c068a1d3115742687b3acd763d1600a247a2d75..7df556827022e6e7c0c0e7d28b6a06533beccecd 100644
--- a/solon-ai-in-quarkus/README.md
+++ b/solon-ai-in-quarkus/README.md
@@ -1 +1,7 @@
-希望有人能提交 pr 完善此示例
\ No newline at end of file
+#### 第一步 启动服务
+```maven
+# -Dquarkus.analytics.disabled=true 的目的是为了启动加速,避免老是提问你是否要参与问卷调查一类的
+mvn quarkus:dev -Dquarkus.analytics.disabled=true
+```
+或者右侧maven有一个启动插件,通过它启动也可以
+
\ No newline at end of file
diff --git a/solon-ai-in-quarkus/img.png b/solon-ai-in-quarkus/img.png
new file mode 100644
index 0000000000000000000000000000000000000000..cae79b8aebbb8f45a20cf85d59c4e7aa1966fbc7
Binary files /dev/null and b/solon-ai-in-quarkus/img.png differ
diff --git a/solon-ai-in-quarkus/pom.xml b/solon-ai-in-quarkus/pom.xml
index f56a38a0b3d784421f859d3eb927d9b8907cd9a8..45c64cd7622b5f9c22461af1d4bab767f9b016e4 100644
--- a/solon-ai-in-quarkus/pom.xml
+++ b/solon-ai-in-quarkus/pom.xml
@@ -12,9 +12,24 @@
17
+
3.28.3
3.6.0
+
+ 3.14.0
+
+ UTF-8
+ UTF-8
+ quarkus-bom
+ io.quarkus.platform
+ 3.28.3
+ true
+ 3.5.4
+
+ 3.6.0
+
+ 5.8.26
@@ -82,43 +97,100 @@
${project.name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- org.apache.maven.plugins
+ ${quarkus.platform.group-id}
+ quarkus-maven-plugin
+ ${quarkus.platform.version}
+ true
+
+
+
+ build
+ generate-code
+ generate-code-tests
+ native-image-agent
+
+
+
+
+
maven-compiler-plugin
- 3.11.0
+ ${compiler-plugin.version}
- -parameters
- ${java.version}
- ${java.version}
- UTF-8
+ true
-
- org.apache.maven.plugins
- maven-assembly-plugin
+ maven-surefire-plugin
+ ${surefire-plugin.version}
- ${project.name}
- false
-
- jar-with-dependencies
-
-
-
- webapp.HelloApp
-
-
+
+ org.jboss.logmanager.LogManager
+ ${maven.home}
+
+
+
+ maven-failsafe-plugin
+ ${surefire-plugin.version}
- make-assembly
- package
- single
+ integration-test
+ verify
+
+
+ ${project.build.directory}/${project.build.finalName}-runner
+
+ org.jboss.logmanager.LogManager
+ ${maven.home}
+
+
@@ -145,4 +217,23 @@
+
+
+
+
+
+ native
+
+
+ native
+
+
+
+ false
+ false
+ true
+
+
+
+
\ No newline at end of file
diff --git a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/DynamicRoutingFilter.java b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/DynamicRoutingFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..75ed001e37dca1dd9319fdbe92950eaf7852825e
--- /dev/null
+++ b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/DynamicRoutingFilter.java
@@ -0,0 +1,131 @@
+package webapp.mcpserver;
+
+import io.quarkus.arc.Arc;
+import io.quarkus.arc.InstanceHandle;
+import io.quarkus.arc.ManagedContext;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.RoutingContext;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.PreMatching;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.Provider;
+import org.noear.solon.web.vertx.VxWebHandler;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+
+@Provider
+@PreMatching // 必须使用这个标记,不然不存在的路径无法执行,直接会被拦截
+public class DynamicRoutingFilter implements ContainerRequestFilter {
+
+
+ @Inject
+ RoutingContext routingContext;
+
+ @Inject
+ Router router;
+
+ @Inject
+ VxWebHandler handler;
+
+
+ @Inject
+ HttpServerRequest request;
+
+ @Override
+ public void filter(ContainerRequestContext ctx) throws IOException {
+//
+// System.out.println(request.params());
+// System.out.println(request.headers());
+//
+// System.out.println(request.body());
+
+
+ String realPath = ctx.getUriInfo().getPath();
+// String method = ctx.getMethod();
+ String patternPath = "/api/hello/:name";
+
+// router.routeWithRegex("/mcp/.*").handler(req -> {
+// // 获取上下文
+// ManagedContext requestContext = Arc.container().requestContext();
+// // 激活上下文
+// requestContext.activate();
+// handler.handle(req.request());
+// });
+
+// router.route(patternPath).handler(rc -> {
+// // 这个的目的是激活 router 对象,当第二次进入的时候,就可以获取到实际的动态 name 了,算是一个bug,所以还是需要直接用 request 直接获取参数即可
+// rc.next();
+// });
+
+// if (realPath.contains("mcp")){
+// // 获取请求上下文
+// ManagedContext requestContext = Arc.container().requestContext();
+// // 激活上下文
+// requestContext.activate();
+// handler.handle(request);
+// }
+
+ // 如果符合规则的话,则会触发实现
+ if(PathMatcher.isMatch(patternPath,realPath)){
+ String target = "org.noear.quarkus.path.HelloNamePath#hello";
+ // 解析 target e.g. "com.example.Hello#hello"
+ String[] parts = target.split("#");
+ String className = parts[0];
+ String methodName = parts[1];
+
+ try {
+ Class> cls = Class.forName(className, false, Thread.currentThread().getContextClassLoader());
+
+ Object bean = null;
+ try {
+ InstanceHandle handle = Arc.container().instance(cls);
+ if (handle != null && handle.isAvailable()) {
+ bean = handle.get();
+ }
+ } catch (Exception ignored) {}
+
+ // 优先尝试接收 ContainerRequestContext
+ try {
+ // 这个就是对应的参数对象方法获取了
+ Method m = cls.getMethod(methodName, RoutingContext.class);
+ // 这个就能触发内部方法,并且quarkus的相应注入对象,就能获取到, 其他拦截前的响应都会失效,但是通过 ctx.abortWith 即可触发 quarkus 自带的一系列后置响应拦截实现
+ Object ret = m.invoke(bean, routingContext);
+ // 如果方法自己写响应可以返回 null 或 void —— 你需要决定如何判断
+ if (ret instanceof Response) {
+ ctx.abortWith((Response) ret);
+ } else if (ret instanceof String) {
+ ctx.abortWith(Response.ok(ret).build());
+ } else {
+ ctx.abortWith(Response.noContent().build());
+ }
+ return;
+ } catch (NoSuchMethodException ex) {
+ // 尝试无参方法
+ Method m = cls.getMethod(methodName);
+ Object ret = m.invoke(bean);
+ if (ret instanceof Response) {
+ ctx.abortWith((Response) ret);
+ } else if (ret instanceof String) {
+ ctx.abortWith(Response.ok(ret).build());
+ } else {
+ ctx.abortWith(Response.noContent().build());
+ }
+ return;
+ }
+ } catch (Throwable t) {
+ // 出错返回 500
+ ctx.abortWith(Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity("dynamic route invoke error: " + t.getMessage()).build());
+ }
+ }
+
+
+
+
+
+ }
+}
diff --git a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/McpServerConfig.java b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/McpServerConfig.java
index 3cf6cb1bf77b4e106430baf2347c8d94b4255de2..bf0b9380e45071db6ffb46bb244448345cdec730 100644
--- a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/McpServerConfig.java
+++ b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/McpServerConfig.java
@@ -1,13 +1,25 @@
package webapp.mcpserver;
+import io.quarkus.arc.Arc;
+import io.quarkus.arc.ManagedContext;
+import io.quarkus.runtime.Startup;
import io.vertx.core.AbstractVerticle;
import io.vertx.ext.web.Router;
+import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Any;
+import jakarta.enterprise.inject.Instance;
+import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.PreMatching;
+import jakarta.ws.rs.ext.Provider;
import org.noear.solon.Solon;
import org.noear.solon.ai.chat.tool.MethodToolProvider;
+import org.noear.solon.ai.embedding.EmbeddingModel;
import org.noear.solon.ai.mcp.McpChannel;
import org.noear.solon.ai.mcp.server.IMcpServerEndpoint;
import org.noear.solon.ai.mcp.server.McpServerEndpointProvider;
@@ -15,31 +27,55 @@ import org.noear.solon.ai.mcp.server.annotation.McpServerEndpoint;
import org.noear.solon.ai.mcp.server.prompt.MethodPromptProvider;
import org.noear.solon.ai.mcp.server.resource.MethodResourceProvider;
import org.noear.solon.web.vertx.VxWebHandler;
+
+import java.util.ArrayList;
import java.util.List;
+import webapp.llm._Constants;
import webapp.mcpserver.tool.McpServerTool2;
/**
* 这个类独立一个目录,可以让 Solon 扫描范围最小化
* */
+@Startup
@ApplicationScoped
-public class McpServerConfig extends AbstractVerticle {
+public class McpServerConfig extends AbstractVerticle {
+
@Inject
Router router;
@Inject
- List serverEndpoints;
+ @Any
+ Instance serverEndpoints;
+
+
+ @Inject
+ VxWebHandler handler;
- private final VxWebHandler handler;
+ @Produces
+ @ApplicationScoped
+ public VxWebHandler handler() {
+ System.out.println("=== VxWebHandler ===");
+ return new VxWebHandler();
+ }
public McpServerConfig() {
- this.handler = new VxWebHandler();
+ // this.handler = new VxWebHandler();
}
+ @PostConstruct
@Override
public void start() {
+ System.out.println("McpServerConfig.start");
router.routeWithRegex("/mcp/.*").handler(req -> {
+ System.out.println("=== mcp ===");
+ // 获取上下文
+// ManagedContext requestContext = Arc.container().requestContext();
+// // 激活上下文
+// requestContext.activate();
handler.handle(req.request());
+ System.out.println("=== mcp end ===");
+// req.next();
});
Solon.start(McpServerConfig.class, new String[]{"--cfg=mcpserver.yml"}, app->{
@@ -76,28 +112,34 @@ public class McpServerConfig extends AbstractVerticle {
}
}
- //Spring 组件转为端点
+ // quarkus 组件转为端点
protected void quarkusCom2Endpoint() {
//提取实现容器里 IMcpServerEndpoint 接口的 bean ,并注册为服务端点
- for (IMcpServerEndpoint serverEndpoint : serverEndpoints) {
- Class> serverEndpointClz = serverEndpoint.getClass();
- McpServerEndpoint anno = serverEndpointClz.getAnnotation(McpServerEndpoint.class);
+ if (serverEndpoints!=null){
- if (anno == null) {
- continue;
- }
+ for (IMcpServerEndpoint serverEndpoint : serverEndpoints) {
+
+ Class> serverEndpointClz = serverEndpoint.getClass();
+ System.out.println("serverEndpoints "+serverEndpointClz.getSimpleName());
+ McpServerEndpoint anno = serverEndpointClz.getAnnotation(McpServerEndpoint.class);
+
+ if (anno == null) {
+ continue;
+ }
- McpServerEndpointProvider serverEndpointProvider = McpServerEndpointProvider.builder()
- .from(serverEndpointClz, anno)
- .build();
+ McpServerEndpointProvider serverEndpointProvider = McpServerEndpointProvider.builder()
+ .from(serverEndpointClz, anno)
+ .build();
- serverEndpointProvider.addTool(new MethodToolProvider(serverEndpointClz, serverEndpoint));
- serverEndpointProvider.addResource(new MethodResourceProvider(serverEndpointClz, serverEndpoint));
- serverEndpointProvider.addPrompt(new MethodPromptProvider(serverEndpointClz, serverEndpoint));
+ serverEndpointProvider.addTool(new MethodToolProvider(serverEndpointClz, serverEndpoint));
+ serverEndpointProvider.addResource(new MethodResourceProvider(serverEndpointClz, serverEndpoint));
+ serverEndpointProvider.addPrompt(new MethodPromptProvider(serverEndpointClz, serverEndpoint));
- serverEndpointProvider.postStart();
+ serverEndpointProvider.postStart();
- //可以再把 serverEndpointProvider 手动转入 SpringBoot 容器
+ //可以再把 serverEndpointProvider 手动转入 SpringBoot 容器
+ }
}
+
}
}
diff --git a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/PathMatcher.java b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/PathMatcher.java
new file mode 100644
index 0000000000000000000000000000000000000000..7720a21664d44fd4007f545709870ae314892516
--- /dev/null
+++ b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/PathMatcher.java
@@ -0,0 +1,46 @@
+package webapp.mcpserver;
+
+public class PathMatcher {
+ public static boolean isMatch(String patternPath, String realPath) {
+ // 分割路径为片段(过滤空字符串)
+ String[] patternSegments = splitPath(patternPath);
+ String[] realSegments = splitPath(realPath);
+
+ // 片段数量不同,直接不匹配
+ if (patternSegments.length != realSegments.length) {
+ return false;
+ }
+
+ // 逐个对比片段
+ for (int i = 0; i < patternSegments.length; i++) {
+ String patternSeg = patternSegments[i];
+ String realSeg = realSegments[i];
+
+ // 动态参数片段(:xxx)可匹配任意非空片段
+ if (patternSeg.startsWith(":")) {
+ // 确保动态参数对应的值非空(根据业务需求可调整)
+ if (realSeg.isEmpty()) {
+ return false;
+ }
+ } else {
+ // 静态片段必须完全相等
+ if (!patternSeg.equals(realSeg)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ // 分割路径为片段,过滤空字符串(处理连续/或首尾/的情况)
+ private static String[] splitPath(String path) {
+ return path.split("/");
+ }
+
+ public static void main(String[] args) {
+ String path = "/api/hello/:name";
+ String realPath = "/api/hello/MrYang";
+ System.out.println(isMatch(path, realPath)); // 输出:true
+ }
+}