diff --git a/solon-ai-in-quarkus/src/main/java/webapp/HelloApp.java b/solon-ai-in-quarkus/src/main/java/webapp/HelloApp.java index c9f92b82bf9a5bca5d2864480e411bba999f332b..04ab4f49287c332e3c2df88258e3bc84522ae6e6 100644 --- a/solon-ai-in-quarkus/src/main/java/webapp/HelloApp.java +++ b/solon-ai-in-quarkus/src/main/java/webapp/HelloApp.java @@ -8,5 +8,6 @@ package webapp; public class HelloApp { public static void main(String[] args) { System.out.println("Hello World!"); + QuarkusApp.main(args); } } diff --git a/solon-ai-in-quarkus/src/main/java/webapp/QuarkusApp.java b/solon-ai-in-quarkus/src/main/java/webapp/QuarkusApp.java new file mode 100644 index 0000000000000000000000000000000000000000..36aef5acdd955a450dfe0cd793139fac2df210cc --- /dev/null +++ b/solon-ai-in-quarkus/src/main/java/webapp/QuarkusApp.java @@ -0,0 +1,38 @@ +package webapp; + + +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.QuarkusApplication; +import io.quarkus.runtime.annotations.QuarkusMain; + +/** + * quarkus 启动类 -- 可以不用设置该也可通过 maven启动 + */ +@QuarkusMain // 标记为Quarkus应用入口 +public class QuarkusApp implements QuarkusApplication { + + + // 可选:如果需要自定义main方法,也可以显式定义(但@QuarkusMain已足够) + public static void main(String[] args) { + Quarkus.run(QuarkusApp.class, args); + } + + // 应用启动时执行的逻辑(在Quarkus容器初始化后运行) + @Override + public int run(String... args) throws Exception { + System.out.println("应用启动成功!自定义启动逻辑执行..."); + + // 示例:启动后执行一些初始化操作 + initResources(); + + // 阻塞当前线程(保持应用运行,除非主动退出) + Quarkus.waitForExit(); + return 0; + } + + private void initResources() { + // 初始化资源(如连接池、缓存等) + } + + +} 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 339b7c8b3d6f4742f41c049807c6e2a0bfe8dabe..771745b4e0e192ceb4198c22c8f7ff5accfabd0c 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 @@ -69,7 +69,7 @@ public class McpServerConfig extends AbstractVerticle { app.filter(new McpServerAuth()); }); - //手动构建 mcp 服务端点(只是演示,可以去掉) -- 目前该模块未打通 quarkus 框架上运行的案例,请使用 quarkusCom2Endpoint() 的方式构建即可 + //手动构建 mcp 服务端点(只是演示,可以去掉) McpServerEndpointProvider endpointProvider = McpServerEndpointProvider.builder() .name("McpServerTool2") .channel(McpChannel.SSE) diff --git a/solon-ai-in-quarkus/src/main/resources/application.properties b/solon-ai-in-quarkus/src/main/resources/application.properties index 50ac10308446d2740b503c1cc9b542aad2930f76..0cf5c48e0e36fcdaeec937d470dbaaa2551a462a 100644 --- a/solon-ai-in-quarkus/src/main/resources/application.properties +++ b/solon-ai-in-quarkus/src/main/resources/application.properties @@ -3,4 +3,7 @@ quarkus.http.port=8080 # 访问前缀 #quarkus.http.root-path=/ # 不移除未使用的class -#quarkus.arc.unremovable-types=org.noear.quarkus.** \ No newline at end of file +#quarkus.arc.unremovable-types=org.noear.quarkus.** + +# 开发时,可绑定本机ip 无绑定时,使用 localhost +#quarkus.http.host=192.168.1.16 \ No newline at end of file diff --git a/solon-ai-in-quarkus/src/test/java/client/McpClientTest.java b/solon-ai-in-quarkus/src/test/java/client/McpClientTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e058b1bbbb236c8b3374caed9c116188c16de272 --- /dev/null +++ b/solon-ai-in-quarkus/src/test/java/client/McpClientTest.java @@ -0,0 +1,174 @@ +package client; + +import io.modelcontextprotocol.spec.McpError; +import org.junit.jupiter.api.Test; +import org.noear.solon.ai.chat.ChatModel; +import org.noear.solon.ai.chat.ChatResponse; +import org.noear.solon.ai.chat.message.ChatMessage; +import org.noear.solon.ai.chat.tool.FunctionTool; +import org.noear.solon.ai.mcp.McpChannel; +import org.noear.solon.ai.mcp.client.McpClientProvider; +import org.noear.solon.rx.SimpleSubscriber; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +// 手工启动 quarkus 后运行测试类即可 +public class McpClientTest { + + String host = "localhost:8080"; + /** + * 工具直接调用 + */ + @Test + public void case1() throws Exception { + + McpClientProvider toolProvider = null; + Map map = null; + String rst = null; + List messageList = null; + String resourceContent = null; + toolProvider = McpClientProvider.builder() + .channel(McpChannel.STREAMABLE) + .apiUrl("http://"+host+"/mcp/demo1/sse") + .build(); + + //工具 + map = Collections.singletonMap("location", "杭州"); + rst = toolProvider.callToolAsText("getWeather", map).getContent(); + System.out.println(rst); + assert "晴,14度".equals(rst); + + //提示语 + messageList = toolProvider.getPromptAsMessages("askQuestion", Collections.singletonMap("topic", "demo")); + System.out.println(messageList); + + //资源 + resourceContent = toolProvider.readResourceAsText("config://app-version").getContent(); + System.out.println(resourceContent); + + resourceContent = toolProvider.readResourceAsText("db://users/12/email").getContent(); + System.out.println(resourceContent); + + System.out.println("---------------"); + + /// ///////////////// + + + toolProvider = McpClientProvider.builder() + .channel(McpChannel.SSE) + .apiUrl("http://"+host+"/mcp/demo2/sse") + .build(); + + //工具 + map = Collections.singletonMap("location", "杭州"); + rst = toolProvider.callToolAsText("getWeather", map).getContent(); + System.out.println(rst); + assert "晴,14度".equals(rst); + + //提示语 + messageList = toolProvider.getPromptAsMessages("askQuestion", Collections.singletonMap("topic", "demo")); + System.out.println(messageList); + + //资源 + resourceContent = toolProvider.readResourceAsText("config://app-version").getContent(); + System.out.println(resourceContent); + + resourceContent = toolProvider.readResourceAsText("db://users/12/email").getContent(); + System.out.println(resourceContent); + } + + //换成自己的模型配置(参考:https://solon.noear.org/article/918) + private static final String apiUrl = "http://127.0.0.1:11434/api/chat"; + private static final String provider = "ollama"; + private static final String model = "qwen2.5:1.5b"; //"llama3.2";//deepseek-r1:1.5b; + + /** + * 与大模型集成 + */ + @Test + public void case2_call() throws Exception { + McpClientProvider toolProvider = McpClientProvider.builder() + .channel(McpChannel.STREAMABLE) + .apiUrl("http://"+host+"/mcp/demo1/sse") + .build(); + + ChatModel chatModel = ChatModel.of(apiUrl) + .provider(provider) + .model(model) + .defaultToolsAdd(toolProvider) //添加默认工具 + .build(); + + ChatResponse resp = chatModel.prompt("杭州今天的天气怎么样?") + .call(); + + System.out.println(resp.getMessage()); + } + + /** + * 与大模型集成 + */ + @Test + public void case2_stream() throws Exception { + McpClientProvider toolProvider = McpClientProvider.builder() + .channel(McpChannel.STREAMABLE) + .apiUrl("http://"+host+"/mcp/demo1/sse") + .build(); + + ChatModel chatModel = ChatModel.of(apiUrl) + .provider(provider) + .model(model) + .defaultToolsAdd(toolProvider) //添加默认工具 + .build(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference errRef = new AtomicReference<>(); + + chatModel.prompt("杭州今天的天气怎么样?") + .stream() + .subscribe(new SimpleSubscriber() + .doOnNext(resp -> { + System.out.println(resp.getMessage().getContent()); + }).doOnError(err -> { + errRef.set(err); + latch.countDown(); + }).doOnComplete(() -> { + latch.countDown(); + })); + + latch.await(); + assert errRef.get() == null; + } + + /** + * 鉴权 + */ + @Test + public void case3_auth() throws Exception { + McpClientProvider mcpClient = McpClientProvider.builder() + .channel(McpChannel.STREAMABLE) + // user 设置为 0 触发 401 效果 + .apiUrl("http://"+host+"/mcp/demo1/sse?user=no") + .build(); + + Throwable error = null; + try { + Collection functionTools = mcpClient.getTools(); + // 获取成功时,返回 工具信息 + for (FunctionTool functionTool : functionTools) { + System.out.println(functionTool); + } + } catch (Throwable e) { + error = e; + e.printStackTrace(); + } + + assert error != null; + assert error instanceof McpError; + assert error.getMessage().contains("401"); + } +} diff --git a/solon-ai-in-quarkus/src/test/java/client/McpStdioDemo.java b/solon-ai-in-quarkus/src/test/java/client/McpStdioDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..981f414e20286497b29969a8449ce5948d1056e8 --- /dev/null +++ b/solon-ai-in-quarkus/src/test/java/client/McpStdioDemo.java @@ -0,0 +1,39 @@ +package client; + +import org.noear.solon.ai.chat.ChatModel; +import org.noear.solon.ai.mcp.McpChannel; +import org.noear.solon.ai.mcp.client.McpClientProvider; +import org.noear.solon.ai.mcp.client.McpServerParameters; + +import java.util.Collections; + + +public class McpStdioDemo { + public static void test() { + //服务端不能开启控制台的日志,不然会污染协议流 + McpClientProvider mcpClient = McpClientProvider.builder() + .channel(McpChannel.STDIO) //表示使用 stdio + .serverParameters(McpServerParameters.builder("npx") + .args("/c", "npx.cmd", "-y", "@modelcontextprotocol/server-everything", "dir") + .build()) + .build(); + + //随便写的,示意一下 + String response = mcpClient.callToolAsText("demo", Collections.singletonMap("p1", "test")) + .getContent(); + + assert response != null; + System.out.println(response); + + mcpClient.close(); + } + + public void demo(McpClientProvider toolProvider) throws Exception { + ChatModel chatModel = ChatModel.of("...") + .defaultToolsAdd(toolProvider) //添加默认工具 + .build(); + + chatModel.prompt("杭州今天的天气怎么样?") + .call(); + } +}