Open Liberty 26.0.0.3-beta 为 mcpServer-1.0 带来了两个重要更新:响应编码器(ContentEncoder / ToolResponseEncoder)和请求ID(RequestId)访问。前者让你完全掌控AI智能体收到的响应结构,后者为日志和审计打开了大门。
如果你已经用过 26.0.0.2-beta 的 MCP Server 功能,你可能遇到过这个问题:工具返回值默认被 JSON-B 序列化成 TextContent,对于复杂业务对象,这意味着 AI 智能体收到的是一大块 JSON 文本,需要自己解析。现在,这个问题有了优雅的解决方案。
简单说:现在你可以精确控制AI智能体收到的数据格式,并实现请求级审计追踪。
为什么这次更新值得关注
上一版(26.0.0.2-beta)解决了”能不能用”的问题——注解驱动、RBAC安全、元数据支持。这一版解决的是”用得好不好”的问题。
本次 26.0.0.3-beta 版本包含以下新特性,与上一版本对比:
Read more: MCP Server 1.0 再进化:自定义响应编码 + 请求级审计,Open Liberty 26.0.0.3-beta 实战解析新特性
| 功能 | 26.0.0.2-beta | 26.0.0.3-beta | 影响范围 |
|---|---|---|---|
| 响应格式控制 | 只能返回JSON-B默认序列化 | 自定义编码器(ToolResponseEncoder/ContentEncoder),精确控制每个Content | 所有返回复杂对象的工具方法 |
| 请求追踪 | 无法区分不同请求 | RequestId注入,端到端可追踪 | 所有需要日志审计的工具方法 |
表1:新特性对比
除了新特性,本次版本还修复了以下两个容易被忽略但会造成实际问题的 Bug:
Bug修复
| 问题 | 状态 | 影响范围 |
|---|---|---|
| 异步工具输出schema生成错误 | 已修复(当structuredContent = true时) | 使用异步工具+结构化内容的场景 |
| 无参工具调用缺少arguments报错 | 已修复(arguments现在可选) | 轻量级AI客户端,或无需参数的工具 |
表2:Bug修复列表
一、响应编码器:让AI智能体收到”对”的数据
问题场景
上一篇文章我们展示的天气工具返回的是String,JSON-B直接当文本返回,没问题。但真实企业应用远比这复杂:
// 搜索结果和条目的定义
public record SearchResult(List<SearchItem> results) {}
public record SearchItem(String summary, double relevance) {}
// 工具方法
@Tool(description = "search the data store")
public SearchResult search(@ToolArg(name="query", description="the query to run") String query) {
SearchResult result = datastore.runQuery(query);
return result;
}
SearchResult包含多个结果条目,每个条目有摘要、元数据和相关度评分。默认情况下,整个对象被JSON-B序列化为一个TextContent——一大坨JSON文本丢给AI。AI智能体需要自己解析这段文本才能理解结构。
这就像你问一个人”搜索结果是什么”,对方把整张数据库表打印出来甩给你。
解决方案:两种编码器
Open Liberty 26.0.0.3-beta提供了两种粒度的编码器:
ToolResponseEncoder<T>:将对象转换为完整的ToolResponse,控制整个响应ContentEncoder<T>:将对象转换为单个Content,适合列表场景(每个元素独立编码)
对于上面的搜索场景,我们用ToolResponseEncoder把每个搜索结果拆成独立的TextContent,并把相关度评分映射为MCP协议的Annotations:
@ApplicationScoped
public class SearchResultEncoder implements ToolResponseEncoder<SearchResult> {
public boolean supports(Class<?> runtimeType) {
return SearchResult.class.isAssignableFrom(runtimeType);
}
public ToolResponse encode(SearchResult searchResult) {
if (searchResult.results().isEmpty()) {
return ToolResponse.error("No results");
}
ArrayList<TextContent> contents = new ArrayList<>();
for (var result : searchResult.results()) {
Annotations annotations = new Annotations(null, null, result.relevance());
contents.add(new TextContent(result.summary(), null, annotations));
}
return ToolResponse.success(contents);
}
}
关键点:这是一个CDI Bean,应用中所有返回SearchResult的工具方法都会自动使用这个编码器。零配置,零侵入。
作为CDI Bean,编码器支持完整的CDI特性。你可以在编码器中@Inject Logger、配置对象等:
@ApplicationScoped
public class SearchResultEncoder implements ToolResponseEncoder<SearchResult> {
@Inject
Logger logger;
public ToolResponse encode(SearchResult searchResult) {
logger.log(INFO, "Encoding " + searchResult.results().size() + " results");
// ...
}
}
AI智能体收到什么
之前:
一个TextContent: {"results":[{"summary":"北京今天晴","relevance":0.95},{"summary":"北京明天多云","relevance":0.88}]}
现在:
TextContent #1: "北京今天晴" (priority: 0.95)
TextContent #2: "北京明天多云" (priority: 0.88)
AI智能体不需要解析JSON,直接拿到结构化的、带优先级标注的内容。这意味着更好的推理质量和更少的token消耗。
ContentEncoder:更轻量的选择
如果你只需要控制单个对象如何变成Content,不需要控制整个响应结构,用ContentEncoder更简洁。当工具方法返回List<T>时,每个元素会独立经过ContentEncoder编码——非常适合一对多场景。
@ApplicationScoped
public class SearchItemEncoder implements ContentEncoder<SearchItem> {
@Override
public boolean supports(Class<?> runtimeType) {
return SearchItem.class.isAssignableFrom(runtimeType);
}
@Override
public Content encode(SearchItem item) {
Annotations annotations = new Annotations(null, null, item.relevance());
return new TextContent(item.summary(), null, annotations);
}
}
关键点:当工具方法返回List<SearchItem>时,每个SearchItem会自动经过SearchItemEncoder编码,无需手动循环。
💡 中文开发者小贴士:MCP 协议传输的是 JSON 文本,字符集问题往往是隐雷。Open Liberty 在 MCP Server 中已原生处理 UTF-8 编码,你在编码器中直接构建
TextContent时无需手动处理字符集,直接传入 String 即可,彻底避免中文乱码问题。
二、RequestId:为AI调用装上”追踪号”
问题场景
在企业环境中,当AI智能体高频调用你的MCP工具时,你需要回答这些问题:
- “这个错误是哪次调用产生的?”
- “过去一小时有多少次查询?”
- “这个敏感操作是谁触发的?”
- “多个请求同时到达时,如何区分哪条日志对应哪次前端操作?”
没有请求级标识,这些问题都无法回答。
解决方案
26.0.0.3-beta让你可以在工具方法中获取每个MCP请求的唯一标识。该标识由客户端(如Claude Desktop)在JSON-RPC请求的id字段中生成并发送,在同一MCP连接内唯一。只需在工具方法中添加RequestId参数即可获取:
@Tool(description = "search the data store")
public SearchResult search(@ToolArg(name="query") String query, RequestId requestId) {
logger.log(INFO, "Running search (" + requestId.asString() + ") for query: " + query);
// ....
}
RequestId由客户端在JSON-RPC请求的id字段中生成,你不需要自己管理。它与上一版的_meta元数据不同——_meta是你主动返回给客户端的,而RequestId是客户端发来的、用于你内部追踪的。
并发场景下的价值:由于MCP是基于JSON-RPC的长连接协议,多个请求可能异步交织到达。此时RequestId是唯一能把后端日志与特定前端操作关联起来的纽带,在调试并发问题时尤为关键。
典型用例
@ApplicationScoped
public class AuditTools {
@RolesAllowed("Admin")
@Tool(name = "deleteUser")
public String deleteUser(
@ToolArg(name = "userId") String id,
RequestId requestId) {
logger.log(WARNING, "Audit: requestId=" + requestId.asString()
+ " action=deleteUser target=" + id);
return userService.delete(id);
}
}
配合上一版的@RolesAllowed,现在你有了谁(角色)在什么时候(RequestId)做了什么(工具+参数)的完整审计链。
架构演进图
从26.0.0.2-beta到26.0.0.3-beta,MCP Server的能力栈:
graph TD
A["AI 智能体 (Claude/Cursor)"] -- "MCP 协议" --> B["Open Liberty MCP Server"]
subgraph Features ["功能演进"]
C1["26.0.0.2 (基础控制)<br/>@Tool, RBAC, _meta"]
C2["26.0.0.3 NEW (深度定制)<br/>Encoder, RequestId"]
end
B --> C1
B --> C2
style C2 fill:#fff3f3,stroke:#ffcccc,stroke-width:2px
style Features fill:none,stroke:#ddd,stroke-dasharray: 5 5三、Bug修复:细节同样重要
这次修复了两个容易被忽略但会造成实际问题的Bug:
- 异步工具的输出schema生成错误:当
structuredContent = true的异步工具方法生成输出schema时,之前版本会生成不正确的schema定义。如果你在用异步工具+结构化内容,升级后行为会修正。 - 无参工具调用缺少arguments对象报错:MCP协议规定
arguments对象在工具无参数时是可选的,但之前的实现强制要求。现在如果工具不需要参数,客户端可以省略arguments——这对轻量级AI客户端更友好。
快速升级指南
如果你已经按照上一篇文章配置了26.0.0.2-beta,升级只需改版本号:
Maven
<plugin>
<groupId>io.openliberty.tools</groupId>
<artifactId>liberty-maven-plugin</artifactId>
<version>3.12.0</version>
<configuration>
<runtimeArtifact>
<groupId>io.openliberty.beta</groupId>
<artifactId>openliberty-runtime</artifactId>
<version>26.0.0.3-beta</version>
<type>zip</type>
</runtimeArtifact>
</configuration>
</plugin>
Gradle
dependencies {
libertyRuntime group: 'io.openliberty.beta', name: 'openliberty-runtime', version: '[26.0.0.3-beta,)'
}
Docker
FROM icr.io/appcafe/open-liberty:beta
实战:构建一个带编码器的搜索工具
把两个新特性组合起来用:
@ApplicationScoped
public class ProductSearchTools {
@Tool(name = "searchProducts",
description = "搜索商品数据库")
public ProductSearchResult search(
@ToolArg(name = "query", description = "搜索关键词") String query,
@ToolArg(name = "category", description = "商品类别") String category,
RequestId requestId) {
logger.log(INFO, "[" + requestId.asString() + "] Searching: "
+ query + " in " + category);
return productService.search(query, category);
}
}
@ApplicationScoped
public class ProductSearchResultEncoder
implements ToolResponseEncoder<ProductSearchResult> {
public boolean supports(Class<?> runtimeType) {
return ProductSearchResult.class.isAssignableFrom(runtimeType);
}
public ToolResponse encode(ProductSearchResult result) {
if (result.products().isEmpty()) {
return ToolResponse.error("未找到匹配商品");
}
ArrayList<TextContent> contents = new ArrayList<>();
for (var product : result.products()) {
Annotations annotations = new Annotations(
null, null, product.relevanceScore());
contents.add(new TextContent(
product.name() + " - ¥" + product.price(),
null, annotations));
}
return ToolResponse.success(contents);
}
}
AI智能体收到的不是一堆JSON,而是每个商品一条独立内容,按相关度排序。同时后台有了完整的请求追踪日志。
使用技巧与最佳实践
Q: ToolResponseEncoder和ContentEncoder该选哪个?
A: 需要控制整个响应结构(比如空结果返回error、添加多个不同类型的Content)选ToolResponseEncoder。只需控制单个对象如何变成Content选ContentEncoder。
Q: 一个类型能同时注册两种编码器吗?
A: 不建议。如果同时存在,ToolResponseEncoder优先。
Q: RequestId是全局唯一的吗?
A: 不是全局唯一,但在同一MCP连接内唯一。适合用于日志关联和审计追踪。
Q: 升级会破坏现有代码吗?
A: 不会。编码器和RequestId都是可选功能。没有使用它们的代码行为完全不变。
Q: 什么时候GA?
A: Beta特性会根据社区反馈决定GA时间。建议在测试环境验证后,关注Open Liberty发布说明获取GA信息。
Q: 编码器抛出异常会怎样?
A: 如果ToolResponseEncoder或ContentEncoder抛出异常,MCP Server会将其作为工具执行错误返回给AI智能体。建议在编码器中做好异常处理,或返回ToolResponse.error()给出友好提示。
Q: 能否在运行时动态切换编码器?
A: 不支持动态切换。编码器是通过CDI容器在启动时静态发现的,运行时不支持切换。如果需要不同场景使用不同编码逻辑,可以在encode方法内根据业务逻辑判断。
Q: 使用编码器会影响性能吗?
A: 编码器会带来轻微的处理延迟(主要是对象序列化的开销)。对于高频调用的工具,建议进行性能测试。如果返回简单类型(如String),使用默认序列化通常更高效。
未来展望:MCP Server 的下一步演进
Open Liberty 的 MCP Server 目前仍处于 beta 阶段,根据社区反馈和官方路线图,后续版本可能会关注以下方向:
- 更多传输层支持:目前主要基于 stdio/SSE,未来可能支持 Streamable HTTP 等新兴传输方式
- 更深的 MicroProfile 整合:与 MicroProfile Telemetry、Fault Tolerance 等标准的深度集成
- 工具发现的动态化:支持运行时注册/注销工具,而不仅限于启动时发现
如果你对某个方向特别感兴趣,欢迎在 Open Liberty 邮件列表 上参与讨论,社区反馈直接影响 GA 版本的功能优先级。
下一步
对于已经在使用26.0.0.2-beta的团队:
- 升级到26.0.0.3-beta,验证异步工具和arguments修复
- 为复杂返回类型实现
ToolResponseEncoder - 在关键工具方法中添加
RequestId参数,建立审计日志 - 在Open Liberty邮件列表反馈使用体验
对于还在观望的团队:现在是从26.0.0.2-beta开始的最好时机——两个版本的新特性加在一起,已经覆盖了从开发到运维的完整链路:
graph LR
subgraph DEV[" 开发阶段 · 26.0.0.2 "]
A["@Tool / @ToolArg<br/><i>注解驱动开发</i>"]
end
subgraph SEC[" 安全阶段 · 26.0.0.2 "]
B["@RolesAllowed / _meta<br/><i>RBAC 鉴权 + 元数据</i>"]
end
subgraph CUSTOM[" 定制阶段 · 26.0.0.3 "]
C("ToolResponseEncoder / ContentEncoder<br/><i>响应格式精确控制</i>")
end
subgraph OPS[" 运维阶段 · 26.0.0.3 "]
D("RequestId 注入<br/><i>审计追踪 + 日志关联</i>")
end
A ==> B ==> C ==> D
style DEV fill:#e3f2fd,stroke:#1565c0,stroke-width:1px
style SEC fill:#fff8e1,stroke:#f9a825,stroke-width:1px
style CUSTOM fill:#e8f5e9,stroke:#2e7d32,stroke-width:3px
style OPS fill:#fce4ec,stroke:#c62828,stroke-width:3px
style A fill:#bbdefb,stroke:#1565c0
style B fill:#ffecb3,stroke:#f9a825
style C fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px
style D fill:#f8bbd0,stroke:#c62828,stroke-width:3px这是一个完整的、可评估的MCP Server方案。
One thought on “MCP Server 1.0 再进化:自定义响应编码 + 请求级审计,Open Liberty 26.0.0.3-beta 实战解析”