☰
jFinal 使用 SolonMCP 开发 MCP:把 endpoint 改到 TaoToken 的 Java8 实践
2026/10/2 6:06:16 网站建设 项目流程

1. jFinal 老项目想接 MCP,Java8 卡在哪一步

如果你手上跑着一个 jFinal 单体应用,JDK 还停在 8,最近又被 MCP(Model Context Protocol)刷屏,想给系统加一个能被大模型调用的工具入口,大概率会先撞上一堵墙:官方mcp-java-sdk要求 Java 17 起步,直接升 JDK 对老项目来说牵一发动全身,Spring 版本、依赖冲突、打包脚本全得跟着动。

SolonMCP(solon-ai-mcp)解决的正是这个尴尬。它是 Solon 生态里的 MCP 扩展,可以内嵌进 jFinal、Vert.x、Spring Boot 2/3 等框架,用接近 MVC 的写法把方法暴露成 MCP 工具、资源和提示词,而且能在 Java8 上跑。换句话说,你不用换框架、不用升 JDK,只要在现有 jFinal 里挂一个 Handler 和一个 Plugin,就能让/mcp/sse这个端点活起来。

这篇面向的是已有 jFinal 单体应用、想快速暴露 MCP 能力的后端开发者。我会把依赖坐标、入口类、路由挂载、endpoint 指向 TaoToken 统一 Key/API 通道的配置片段全部写出来,再给一次本地调用验证和返回结果核对清单。适合谁:能改 jFinal 配置、会看 Maven 依赖、想用最小改动试水 MCP 的后端同学。不适合谁:完全没接触过 jFinal 生命周期、也不打算读配置的人。

先说清楚一个概念,避免后面混淆。MCP 服务端(Server)负责暴露能力,MCP 客户端(Client)负责调用能力,而大模型(LLM)是最终消费方。SolonMCP 同时提供了服务端注解和客户端McpClientProvider,所以你可以先在本机把服务端跑起来,再用客户端验证,最后把客户端接到 TaoToken 的统一通道上,让模型通过标准接口来调你的 jFinal 业务方法。

我试过在一台只装了 JDK8 的机器上从零搭这个链路,踩的坑主要集中在异步支持和 endpoint 路径匹配上,下面按顺序讲。

2. TaoToken 前置准备:统一 Key 与 API 通道

在动手改 jFinal 之前,先把外部通道准备好。MCP 客户端最终要调用大模型,如果每个模型都单独配一套 Key 和地址,代码里会散落一堆常量,换模型就得改代码。TaoToken 提供的是统一 Key 和统一 API 通道,客户端只需要认一个 Base URL 和一个 Key,模型 ID 作为参数传入,切换模型时改一个字符串就行。

你需要准备三样东西,我把它叫做「三件套」,后面所有配置都围绕它展开:

项目值说明
Base URLhttps://taotoken.net/api统一 API 通道地址,不加任何查询参数
API Key在控制台创建形如sk-开头的一串字符,只显示一次
Model ID例如claude-sonnet-4-5等具体以文档里的模型列表为准

获取路径很直接:打开官网https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,注册登录后进入控制台,在 API Keys 页面创建一个新 Key。创建时建议按用途命名,比如jfinal-mcp-dev,方便以后区分环境和轮换。Key 只在创建时完整显示一次,复制后先存到本地环境变量或配置中心,别直接硬编码进 Git 仓库。

注意:Base URL 用https://taotoken.net/api,不要在后面拼/v1之类的后缀,也不要带 UTM 参数,客户端拼接路径时容易出问题。

控制台里还能看到模型列表和用量统计。模型 ID 建议先用文档里标注的稳定版本,别一上来就选实验性模型,排查问题时变量太多。如果你只是本地验证链路通不通,选一个响应快的轻量模型即可,等链路跑通再换更强的模型做实际业务。

把这三件套写进一个本地配置文件,比如mcpserver.yml同级放一个llm.properties,或者直接用环境变量注入。环境变量的好处是打包镜像时不用改文件,坏处是本地调试要记得 export。我一般两种都留:配置文件写默认值,环境变量可覆盖。

export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_API_KEY="sk-你的Key" export TAOTOKEN_MODEL="claude-sonnet-4-5"

到这里前置就绪。接下来进入 jFinal 侧的实际改造,重点是让 Solon 容器和 jFinal 容器共存,并且把/mcp/开头的请求交给 Solon 处理。

3. 可复制配置:依赖、入口类与 endpoint 挂载

这一节是全文的核心,所有片段都可以直接复制。先加 Maven 依赖,solon-ai-mcp的版本以你仓库里能拉到的最新稳定版为准,写死一个你验证过的版本号,别用LATEST。

<dependency> <groupId>org.noear</groupId> <artifactId>solon-ai-mcp</artifactId> <version>3.x.x</version> </dependency>

然后是 jFinal 入口类。关键点有两个:一是onDeploy里给 jfinal 过滤器开启异步支持,MCP 内部基于响应式,不开异步会直接卡住;二是把McpServerConfig同时注册为 Plugin 和 Handler,Plugin 负责 Solon 生命周期,Handler 负责请求转发。

public class HelloApp extends JFinalConfig { public static void main(String[] args) { UndertowServer.create(HelloApp.class) .setDevMode(false) .setPort(8080) .onDeploy((cl, di) -> { // 关键:MCP 基于响应式,必须开启异步支持 di.getFilters().get("jfinal").setAsyncSupported(true); }) .start(); } public void configConstant(Constants me) { me.setDevMode(false); } public void configRoute(Routes me) { // 业务路由照常写,MCP 走 Handler 不走 Route } public void configEngine(Engine me) { } public void configPlugin(Plugins me) { me.add(mcpServerConfig); } public void configInterceptor(Interceptors me) { } public void configHandler(Handlers me) { me.add(mcpServerConfig); } private McpServerConfig mcpServerConfig = new McpServerConfig(); }

接着是McpServerConfig,它继承Handler并实现IPlugin。start()里启动 Solon 并指定配置文件,handle()里判断路径前缀,命中/mcp/就交给 Solon 处理,否则放行给下一个 Handler。这里有个细节:isHandled[0] = true一定要设,否则 jFinal 会继续往下找处理器,导致重复响应。

public class McpServerConfig extends Handler implements IPlugin { public boolean start() { Solon.start(McpServerConfig.class, new String[]{"--cfg=mcpserver.yml"}); return true; } public boolean stop() { if (Solon.app() != null) { Solon.stopBlock(false, Solon.cfg().stopDelay()); } return true; } @Override public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { if (target.startsWith("/mcp/")) { Context ctx = new SolonServletContext(request, response); try { Solon.app().tryHandle(ctx); if (isHandled != null && isHandled.length > 0) { isHandled[0] = true; } } catch (Throwable e) { ctx.errors = e; throw e; } finally { ContextUtil.currentRemove(); } } else { if (next != null) { next.handle(target, request, response, isHandled); } } } }

然后是 MCP 服务端点类,用@McpServerEndpoint声明 SSE 路径,方法上用@ToolMapping、@ResourceMapping、@PromptMapping分别暴露工具、资源和提示词。编译时建议加-parameters参数,否则参数名会丢失,得在每个@Param里手写 name。

@McpServerEndpoint(sseEndpoint = "/mcp/sse") public class McpServer { @ToolMapping(description = "查询天气预报") public String getWeather(@Param(description = "城市位置") String location) { return "晴,14度"; } @ResourceMapping(uri = "config://app-version", description = "获取应用版本号") public String getAppVersion() { return "v3.2.0"; } @ResourceMapping(uri = "db://users/{user_id}/email", description = "根据用户ID查询邮箱") public String getEmail(@Param(description = "用户Id") String user_id) { return user_id + "@example.com"; } @PromptMapping(description = "生成关于某个主题的提问") public Collection<ChatMessage> askQuestion(@Param(description = "主题") String topic) { return Arrays.asList( ChatMessage.ofUser("请解释一下'" + topic + "'的概念?") ); } }

最后是mcpserver.yml,把 Solon 的端口和 MCP 相关配置写进去。注意这个端口是 Solon 内部用的,jFinal 对外还是 8080,请求通过 Handler 转发进来。

server: port: 8081 solon: app: name: jfinal-mcp-server

Maven 编译参数别忘了加:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <compilerArgs> <arg>-parameters</arg> </compilerArgs> </configuration> </plugin>

到这里配置就齐了。启动HelloApp.main,控制台看到 Solon 启动日志和 jFinal 启动日志都打印出来,说明两个容器共存成功。下一步做验证。

4. 验证请求:本地调用与返回结果核对

验证分两层:先验证 MCP 服务端本身能响应,再验证客户端能通过 TaoToken 通道让模型调用到你的工具。

第一层,直接用McpClientProvider连本地 SSE 端点,调用getWeather工具,核对返回是不是晴,14度。

public class McpClientTest { public static void main(String[] args) throws Exception { McpClientProvider toolProvider = McpClientProvider.builder() .apiUrl("http://localhost:8080/mcp/sse") .build(); Map<String, Object> map = Collections.singletonMap("location", "杭州"); String rst = toolProvider.callToolAsText("getWeather", map).getContent(); System.out.println(rst); assert "晴,14度".equals(rst); String version = toolProvider.readResourceAsText("config://app-version").getContent(); System.out.println(version); } }

跑通后控制台应该依次输出晴,14度和v3.2.0。如果assert没抛异常,说明工具调用和资源读取两条链路都通了。

第二层,把 MCP 客户端作为工具集挂到 ChatModel 上,让模型自己决定调哪个工具。这里用 TaoToken 的统一通道,Base URL 填https://taotoken.net/api,Key 从环境变量读,模型 ID 用你准备好的那个。

public class McpWithLlmTest { public static void main(String[] args) throws Exception { String baseUrl = System.getenv("TAOTOKEN_BASE_URL"); String apiKey = System.getenv("TAOTOKEN_API_KEY"); String model = System.getenv("TAOTOKEN_MODEL"); McpClientProvider toolProvider = McpClientProvider.builder() .apiUrl("http://localhost:8080/mcp/sse") .build(); ChatModel chatModel = ChatModel.of(baseUrl + "/chat/completions") .apiKey(apiKey) .model(model) .defaultToolsAdd(toolProvider) .build(); ChatResponse resp = chatModel.prompt("杭州今天的天气怎么样?").call(); System.out.println(resp.getMessage()); } }

核对清单如下,逐条对:

检查项期望结果不符时看哪
Solon 启动日志打印 app name 和端口mcpserver.yml路径
jFinal 启动日志打印路由和端口 8080HelloApp配置
/mcp/sse可访问返回 SSE 事件流Handler 路径前缀
工具调用返回晴,14度@ToolMapping方法
资源读取返回v3.2.0@ResourceMappinguri
模型回复含天气提到晴、14 度三件套配置

模型回复里如果出现了「晴,14度」这类信息,说明它确实调用了你的 jFinal 方法,而不是自己编的。这一步是整个链路打通的标志。

提示:验证阶段把模型温度调低,减少它自由发挥的概率,更容易判断工具是否真的被调用。

5. 常见报错排查:401、local proxy failed 与 reading choices

链路跑不通时,报错信息往往指向几个固定位置。下面按我实际遇到的顺序列出来,对照着查。

401 Unauthorized。最常见的原因是 Key 没读到或读错了。先确认环境变量在当前 shell 里echo $TAOTOKEN_API_KEY有值,再确认代码里读的是同一个变量名。如果 Key 是从配置文件读的,检查有没有多余空格或换行。还有一种情况是 Key 被禁用或额度耗尽,去控制台看用量。注意 Base URL 别写成带/v1的旧习惯,统一用https://taotoken.net/api。

local proxy failed / connection refused。这个报错通常出现在客户端连本地 MCP 服务端时,说明http://localhost:8080/mcp/sse根本没起来。检查三件事:HelloApp.main是否真的启动成功、8080 端口是否被占用、target.startsWith("/mcp/")的路径是否和@McpServerEndpoint的sseEndpoint一致。我踩过的坑是sseEndpoint写成/sse,但 Handler 判断的是/mcp/,结果请求进来直接被放行给下一个 Handler,客户端一直等不到响应。

reading choices 相关报错。这类报错一般出现在解析模型响应时,说明返回结构不是预期的 chat completion 格式。先确认请求地址拼的是baseUrl + "/chat/completions",再确认模型 ID 在 TaoToken 的模型列表里存在。如果模型 ID 写错,有些通道会返回错误结构而不是标准 404,解析时就会在choices字段上报错。把模型 ID 换成文档里明确列出的稳定版本再试。

OAuth / 认证方式不匹配。如果你之前配过别的客户端,可能残留了 OAuth 相关的配置项,而 TaoToken 走的是 API Key 认证。检查配置文件里有没有多余的authType、oauth字段,删掉后只保留apiKey。Claude Code 这类工具如果之前配过别的通道,也要把旧的认证配置清干净,否则会优先走旧配置。

异步未开启导致的挂起。表现是请求发出去后一直不返回,也不报错。回到HelloApp.onDeploy,确认di.getFilters().get("jfinal").setAsyncSupported(true)这行真的执行了。如果 jFinal 版本较老,过滤器名字可能不是jfinal,打印一下di.getFilters()的 key 集合确认。

参数名为空。工具调用时报参数缺失,但客户端明明传了。这是编译时没加-parameters导致的,方法参数名被编译成arg0、arg1。两个解法:加编译参数,或者在每个@Param里显式写name。

排查顺序建议从内到外:先确认本地 MCP 服务端能被McpClientProvider直接调用,再确认模型能通过 TaoToken 通道回复,最后才怀疑业务逻辑。这样能把问题范围快速缩小到某一层。

6. 把链路固定下来:接入文档与后续动作

链路跑通之后,建议把配置固化,别每次靠记忆。三件套写进配置中心或环境变量模板,mcpserver.yml和HelloApp的异步设置写进项目 README,新同学拉下来就能跑。

如果你在排障阶段卡在认证或路径上,直接对照接入文档里的示例再核一遍,比反复猜快得多。需要新建或轮换 Key 时,去 API Keys 页面操作,别在代码里改。想先确认模型通道本身是否正常,可以先用模型对话页面发一条消息,排除掉模型侧的问题,再回来查 MCP 侧。

后续要扩展的话,方向有几个:把@ToolMapping方法接到真实的 jFinal Service 上,让模型能查真实业务数据;给不同环境配不同的 Key,用命名区分;把McpServerConfig的路径前缀做成可配置项,避免和现有路由冲突。每加一个工具,就补一条验证用例,保证工具描述和实际行为一致,模型才不会调错。

最后留一个实用技巧:工具方法的description写得越具体,模型选对工具的概率越高。别写「查询数据」,写「根据用户 ID 查询邮箱地址,输入为纯数字字符串」。这个细节比调模型参数更影响实际效果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询