1. jFinal 项目接入 SolonMCP 的真实痛点与场景拆解
如果你正在维护一个 jFinal 项目,又想让它在不升级 JDK 的前提下对外提供 MCP 服务,那你大概率已经踩过这个坑:官方mcp-java-sdk要求 Java 17 起步,而不少存量 jFinal 项目还跑在 Java 8 上,硬升级 JDK 意味着要重新验证一堆老依赖,成本高得离谱。SolonMCP(也就是solon-ai-mcp)解决的正是这个问题——它把 MCP 服务端封装成类似 MVC 的开发风格,能内嵌进 jFinal、Vert.x、Spring Boot 2/3 等框架,Java 8 就能跑起来。
MCP 本身是模型上下文协议,简单说就是让大模型能调用你系统里的工具、读取资源、使用提示模板的一套标准接口。你写一个@ToolMapping方法,模型就能在对话里调用它,比如查天气、查订单、读配置。对 Java 开发者来说,这意味着可以把现有业务能力快速暴露给 AI 客户端。
但真正落地时,问题往往不在 MCP 协议本身,而在“模型从哪来、Key 怎么管、调用链路怎么验证”。本地联调阶段,你可能同时要接 Ollama、接云端模型、接不同厂商的 API,每个都配一套 Key 和 Base URL,改起来很烦。这篇就聚焦 jFinal + SolonMCP 的接入配置角度,给出 TaoToken 统一 Key/API 通道的 config 骨架,以及 SolonMCP 侧的配置片段,最后附一次可复制的请求验证动作,确认整条 MCP 调用链路是通的。
适合谁看:手上有 jFinal 项目、想低成本试水 MCP 的 Java 后端;正在做本地联调、被多套 Key 配置搞烦的开发者;以及想搞清楚 SolonMCP 到底怎么嵌进非 Solon 框架的人。下面从依赖开始,一步步把配置和验证做完。
2. TaoToken 统一 Key 与 API 通道的前置准备
在动手改 jFinal 之前,先把“模型侧”的通道准备好。本地联调最容易乱的地方就是:MCP 服务端写好了,但客户端调模型时不知道用哪个 Base URL、哪个 Key、哪个 Model ID。TaoToken 在这里的作用是提供一个统一的 API 通道,你只需要维护一套 Key,就能在模型对话、编码 Agent、MCP 工具调用等场景里复用。
先明确三个核心概念,后面配置里会反复出现:
Base URL 是请求的入口地址,OpenAI 兼容风格一般是https://taotoken.net/api,注意这个地址不带任何查询参数,保持干净。API Key 是你的身份凭证,在控制台生成,形如sk-开头的一串字符。Model ID 是你要调用的具体模型标识,比如gpt-4o、claude-3-5-sonnet这类,具体以你账号下可用的为准。
获取 Key 的路径很直接:打开控制台,进入 API Keys 页面,新建一个 Key 并复制保存。这里有个实操建议——本地联调单独建一个 Key,别和线上共用,方便出问题时快速吊销。控制台地址是https://taotoken.net/console,API Keys 页面是https://taotoken.net/api-keys。
如果你只是想先验证模型通道是否通,可以先用模型对话页面发一条消息试试,地址是https://taotoken.net/chat。这一步能确认 Key 和 Base URL 没问题,再去配 jFinal 会省很多排查时间。
对于长期做编码或 Agent 的场景,可以考虑 Coding Plan,它更适合高频调用;而单纯的接入排障,优先看 API Keys 和接入文档。文档地址是https://taotoken.net/doc,里面有针对不同语言和框架的示例。
把这三样东西记下来:Base URL、API Key、Model ID。接下来在 jFinal 项目里,我们会把它们写进配置文件,让 SolonMCP 的客户端部分能读到。注意,MCP 服务端本身不直接调模型,真正调模型的是“把 MCP 客户端当工具集用”的那一层,所以 Key 的配置要放在客户端侧,而不是服务端。
3. jFinal + SolonMCP 的可复制配置骨架
这一节是重点,给出可以直接抄的配置。先看 Maven 依赖,solon-ai-mcp是核心包,版本以你实际拉到的为准:
<dependency> <groupId>org.noear</groupId> <artifactId>solon-ai-mcp</artifactId> <version>3.x.x</version> </dependency>然后是 jFinal 的入口类,关键是开启异步支持,因为 MCP 内部基于响应式:
public class HelloApp extends JFinalConfig { public static void main(String[] args) { UndertowServer.create(HelloApp.class) .setDevMode(false) .setPort(8080) .onDeploy((cl, di) -> { di.getFilters().get("jfinal").setAsyncSupported(true); }).start(); } public void configConstant(Constants me) { me.setDevMode(false); } public void configRoute(Routes me) { } 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,负责把/mcp/开头的请求交给 Solon 处理:
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); } } } }接下来是mcpserver.yml,这里放 SolonMCP 的服务端配置,同时把 TaoToken 的通道信息也集中管理。注意 YAML 里不要出现任何代理相关字段,只保留标准配置:
solon: app: name: jfinal-mcp-server mcp: server: sseEndpoint: /mcp/sse llm: baseUrl: https://taotoken.net/api apiKey: sk-你的Key model: gpt-4o如果你更习惯用 JSON 管理,也可以写成mcpserver.json,字段名保持一致:
{ "solon.app.name": "jfinal-mcp-server", "mcp.server.sseEndpoint": "/mcp/sse", "llm.baseUrl": "https://taotoken.net/api", "llm.apiKey": "sk-你的Key", "llm.model": "gpt-4o" }工具端点类用注解声明,@McpServerEndpoint指定 SSE 路径,@ToolMapping暴露工具方法:
@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"; } }编译参数建议加上-parameters,否则@Param的 name 最好显式写全,不然反射拿不到参数名。到这里,服务端和通道配置就齐了,三件套 Base URL、Key、Model ID 都在mcpserver.yml里统一维护。
4. 一次可复制的请求验证与成功结果确认
配置写完,必须验证链路。先启动HelloApp的 main 方法,控制台看到 Solon 启动日志、端口 8080 监听成功,说明服务端起来了。然后写一个客户端测试类,直接调 MCP 工具:
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,说明 MCP 服务端和工具调用链路是通的。这一步不涉及模型,纯粹验证 MCP 协议层。
接下来验证“MCP 客户端作为 LLM 工具集”的完整链路,也就是模型通过工具调用你的 MCP 服务。这里用 TaoToken 的通道:
public class McpLlmTest { public static void main(String[] args) throws Exception { McpClientProvider toolProvider = McpClientProvider.builder() .apiUrl("http://localhost:8080/mcp/sse") .build(); ChatModel chatModel = ChatModel.of("https://taotoken.net/api") .provider("openai") .model("gpt-4o") .apiKey("sk-你的Key") .defaultToolsAdd(toolProvider) .build(); ChatResponse resp = chatModel.prompt("杭州今天的天气怎么样?").call(); System.out.println(resp.getMessage()); } }成功的结果是:模型返回的内容里包含“晴,14度”这个来自你 MCP 工具的结果,而不是模型自己编的天气。这说明模型正确识别了工具、发起了调用、拿到了返回值并组织成自然语言。如果只返回一段泛泛的天气描述,没有调用工具,那就要检查defaultToolsAdd是否生效、工具描述是否清晰。
验证通过后,整条链路就是:客户端请求 → TaoToken 通道 → 模型决策 → 回调本地 MCP 服务 → 返回工具结果 → 模型组织回复。你可以把这段测试代码留在项目里,作为回归验证用。
5. 联调常见报错排查对照
本地联调最容易撞的几个报错,这里逐个对照。
第一个是 401 Unauthorized。表现是模型调用直接返回鉴权失败。原因通常是apiKey没配、配错,或者 Key 被吊销。排查动作:确认mcpserver.yml里的llm.apiKey是sk-开头且没有多余空格;去控制台 API Keys 页面确认这个 Key 还在有效状态;如果刚生成,等几秒再试。注意不要把 Key 写进代码里提交到仓库,用配置文件或环境变量。
第二个是 local proxy failed 或连接被拒绝。表现是请求发不出去,报连接层错误。原因一般是 Base URL 写错,比如多写了路径、带了多余斜杠,或者本地网络环境有干扰。排查动作:确认llm.baseUrl就是https://taotoken.net/api,不要加/v1之类的后缀;用 curl 直接测一下这个地址是否可达。这里要强调,任何涉及网络代理的配置都不要加,保持直连标准配置即可。
第三个是 reading choices 相关的解析错误。表现是返回体解析失败,提示读不到choices字段。原因通常是 Base URL 指向了非 OpenAI 兼容的端点,或者 Model ID 写错导致返回了错误结构。排查动作:确认provider设为openai,model用账号下真实可用的 ID;先用模型对话页面发一条消息,确认这个 Model ID 能正常返回标准结构。
第四个是 OAuth 或鉴权流程报错。表现是提示需要 OAuth、token 无效。原因可能是误用了需要 OAuth 的端点,而 TaoToken 走的是 API Key 鉴权。排查动作:确认你用的是 API Key 而不是 OAuth token;检查请求头里是不是混入了其他鉴权信息。如果你在用 Claude Code 这类工具,注意它的配置和纯 API 调用不同,Claude Code 的接入文档在https://taotoken.net/doc里有单独说明。
第五个是 MCP 工具没被调用。表现是模型回复正常,但内容不是工具返回的。原因通常是工具描述太模糊,或者defaultToolsAdd没加上。排查动作:把@ToolMapping的 description 写具体,比如“查询指定城市的实时天气”;确认defaultToolsAdd(toolProvider)在 build 之前调用。
第六个是异步支持没开导致的阻塞。表现是请求卡住不返回。原因就是 jFinal 的 filter 没设setAsyncSupported(true)。排查动作:回到HelloApp的onDeploy里确认那行代码在。
把这几条对照着查,基本能覆盖本地联调 90% 的问题。每解决一个,链路就稳一分。
6. 从本地联调到稳定使用的下一步
链路通了之后,下一步是把配置固化下来。建议把mcpserver.yml里的 Key 换成环境变量读取,比如${TAOTOKEN_API_KEY},这样本地和部署环境可以共用一份配置。Solon 的配置支持占位符,改起来不麻烦。
工具方法这边,随着业务变多,@ToolMapping会越来越多,建议按业务域拆成多个@McpServerEndpoint类,每个类管一组相关工具,别全堆在一个类里。资源读取用@ResourceMapping,适合暴露配置、版本号这类只读数据;提示模板用@PromptMapping,适合把常用提问封装成模板。
如果你后面要接 Claude Code 或做更复杂的 Agent 编排,可以看 Coding Plan,它更适合长期编码场景。而单纯的接入和排障,API Keys 加接入文档就够了。文档里还有针对不同框架的完整示例,遇到配置细节可以直接对照。
最后留一个实操习惯:每次改完配置,先跑一遍第 4 节那个McpClientTest,确认 MCP 协议层没坏,再跑McpLlmTest确认模型通道没坏。两步都过,再动业务代码。这样出问题时能快速定位是协议层还是模型层,比一股脑排查省时间。