一、引言:我的 AI 能力版图

在深入本文之前,先交代一下我目前在 Java AI 领域的四项目布局。这四个开源项目各有侧重,共同构成了一个从框架原理到工程落地的完整探索路径:

项目 聚焦方向 核心框架 语言
whatsmars LangChain4j 深度探索 LangChain4j Java
spacecloud Spring AI 2.0 微服务应用 Spring AI 2.0 Java
babi AI Coding Agent 工程化 AgentScope / LangGraph Java / Python
jaws RPC 转 MCP 服务 MCP Java SDK Java

其中 whatsmars 深耕 LangChain4j 生态原理,spacecloud 聚焦 Spring AI 2.0 在微服务场景的应用,babi 探索 Agent 工程化落地,而 jaws 作为一个基于 Netty 的高性能 RPC 框架,最近完成了一次重要进化——将 RPC 服务自动暴露为 MCP Tools 和 REST API,打通了传统后端服务与 AI Agent 之间的调用壁垒。

项目地址:

  • https://github.com/javahongxi/whatsmars
  • https://github.com/javahongxi/spacecloud
  • https://github.com/javahongxi/babi
  • https://github.com/javahongxi/jaws

今天本文要拆解的,就是 jaws 新增的 MCP 桥接和 REST 桥接两大特性。

二、jaws 框架回顾与 MCP 协议背景

2.1 jaws 框架回顾

jaws 是一个基于 Java 17 和 Netty 的高性能 RPC 框架,经过持续迭代,已具备相当完整的微服务通信能力。在协议层,jaws 定义了自定义二进制协议,支持 fastjson2 和 hessian2 两种序列化方式,底层基于 Netty 的 EventLoop 实现高性能网络传输。在服务治理层,jaws 支持 ZooKeeper 和 Nacos 两种注册中心,内置心跳续约与失败重连机制,提供 random、roundRobin、leastActive、shortestResponse、consistentHash 五种负载均衡策略,以及 failover(失败切换)、failfast(快速失败)、failback(异步重试)三种高可用容错模式。在扩展性方面,所有核心组件(Protocol、Cluster、LoadBalance、Filter、Serialization 等)均通过 SPI 机制可插拔替换。此外还提供优雅停机(四阶段停机保证零损伤发布)、泛化调用(无需接口 JAR 包即可发起调用)、配置热更新(运行时动态调整负载均衡、容错策略、超时、重试等参数)、流量调度(跨分组流量调度,支持按权重合并多 group 服务、IP 路由规则,灰度发布利器)等高级特性。

2.2 MCP 协议与 AI Agent 的调用困境

MCP(Model Context Protocol)是 Anthropic 于 2024 年底发布的开放协议,旨在为 AI 模型(LLM)提供一种标准化的外部工具调用方式。在 MCP 出现之前,每个 AI 应用需要为不同的工具(数据库查询、文件操作、API 调用等)编写定制化的集成代码,缺乏统一标准。MCP 通过 JSON-RPC 2.0 定义了一套 Client-Server 交互协议,Server 声明自己能提供的 Tools(工具)、Resources(资源)和 Prompts(提示模板),Client(通常是 AI Agent 运行时)在对话过程中根据需要调用这些能力。

MCP 协议的交互流程分为几个阶段:首先是 initialize 握手,Client 和 Server 协商协议版本和各自的能力声明;接着 Client 发送 initialized 通知表示握手完成;此后 Client 可以随时调用 tools/list 获取可用工具列表,调用 tools/call 执行某个工具。Server 也可以通过 notifications/tools/list_changed 主动通知 Client 工具列表发生了变化。MCP 支持两种传输方式:stdio(标准输入输出,适用于本地进程间通信)和 HTTP+SSE(Streamable HTTP,适用于远程网络通信)。jaws 的 MCP 桥接使用的是后者,具体基于 MCP Java SDK 2.0.0 的 HttpServletStreamableServerTransportProvider,以 Servlet 方式嵌入 Spring Boot 应用。

理解了 MCP 协议的设计意图,就能明白 jaws MCP 桥接要解决的核心问题:在 AI 时代,Agent 是新的调用方,它不会也不应该理解 Jaws 的二进制协议、服务注册中心、负载均衡等细节。Agent 只会说 MCP——一种基于 JSON-RPC 的标准工具调用协议。如何让已有的 RPC 服务以零改造的方式出现在 Agent 的工具列表中?答案就是在 Jaws 框架内部架一座桥,自动将 RPC 方法翻译成 MCP Tool。

三、MCP Java SDK 架构深度解析

jaws 的 MCP 桥接建立在 MCP Java SDK(io.modelcontextprotocol.sdk)之上。要理解桥接的设计决策,有必要先深入 SDK 的架构。SDK 版本为 2.0.0,由 Anthropic 维护,Java 17 编译,采用 MIT 协议开源。

3.1 模块结构:核心与 JSON 绑定的解耦

SDK 采用多模块设计,核心模块 mcp-core 不依赖任何具体的 JSON 库——它只引用了 jackson-annotations(注解接口),实际的数据绑定通过 SPI 机制在 mcp-json-jackson2mcp-json-jackson3 中实现。这意味着如果项目已经用了 Jackson 2,就引入 jackson2 绑定;如果用 Jackson 3,就引入 jackson3 绑定;理论上甚至可以自己实现一个 Gson 或 JSON-B 的绑定模块。jaws 选择的是 mcp-json-jackson2,配合 fastjson2 作为业务序列化层,两者各司其职。

模块概览如下:

mcp-bom             → BOM,统一版本管理
mcp                 → 便捷聚合包(mcp-core + mcp-json-jackson3)
mcp-core            → 核心SDK,传输层无关,JSON通过SPI注入
mcp-json-jackson2   → Jackson 2 绑定 + JSON Schema Validator
mcp-json-jackson3   → Jackson 3 绑定 + JSON Schema Validator
mcp-test            → 测试基础设施

mcp-core 对外暴露的核心包是 io.modelcontextprotocol.server(服务端 API)和 io.modelcontextprotocol.spec(协议 schema)。值得注意的是 mcp-corepom.xmljakarta.servlet-api 的 scope 是 provided——Servlet 传输层编译时需要它,但运行时由宿主容器提供。这保证了 mcp-core 可以嵌入 Spring Boot、纯 Servlet 容器甚至 OSGi 环境。

3.2 McpServer:六种工厂方法,三种服务器家族

McpServer 是 SDK 的入口,它是一个接口,通过六个静态工厂方法返回不同的 Builder:

工厂方法 传输层类型 返回的 Builder 会话模型
sync(McpServerTransportProvider) 经典单会话 SingleSessionSyncSpecification 一个 McpServerSession
async(McpServerTransportProvider) 经典单会话 SingleSessionAsyncSpecification 一个 McpServerSession
sync(McpStreamableServerTransportProvider) Streamable HTTP StreamableSyncSpecification 多个 McpStreamableServerSession
async(McpStreamableServerTransportProvider) Streamable HTTP StreamableServerAsyncSpecification 多个 McpStreamableServerSession
sync(McpStatelessServerTransport) 无状态 StatelessSyncSpecification 无会话
async(McpStatelessServerTransport) 无状态 StatelessAsyncSpecification 无会话

这三种服务器家族对应 MCP 协议的三种传输模式:经典单会话(STDIO)、Streamable HTTP 多会话、无状态。jaws 使用的是第三种工厂方法 McpServer.sync(McpStreamableServerTransportProvider),因为 jaws 的 MCP 服务部署在 Servlet 容器中,需要支持多个 AI Agent 客户端同时连接。

3.3 McpSyncServer:异步核心的阻塞外壳

SDK 的核心实现是 McpAsyncServer——一个基于 Project Reactor 的响应式服务器。McpSyncServer 只是对 McpAsyncServer 的一层薄封装,每个方法都是 asyncServer.X(...).block()

public class McpSyncServer {
    private final McpAsyncServer asyncServer;
    private final boolean immediateExecution;

    public void addTool(McpServerFeatures.SyncToolSpecification toolHandler) {
        this.asyncServer
            .addTool(McpServerFeatures.AsyncToolSpecification.fromSync(toolHandler, this.immediateExecution))
            .block();
    }
    // ...每个方法都是 asyncServer.X(...).block()
}

McpServerFeatures.AsyncToolSpecification.fromSync() 做的转换很关键——它将同步的 BiFunction 包装成 Mono.fromCallable(...),并根据 immediateExecution 标志决定是否切换到 Schedulers.boundedElastic() 线程池执行。默认 immediateExecution=false,意味着每个同步 handler 都会在独立的弹性线程池中执行,避免阻塞 Netty 的事件循环线程。jaws 在构建时使用了默认配置,这确保了即使某个 RPC 调用耗时较长,也不会阻塞其他 MCP 会话。

3.4 HttpServletStreamableServerTransportProvider:Servlet 即传输层

这是 jaws MCP 桥接依赖的核心传输层实现。它本身就是一个 HttpServlet,标注了 @WebServlet(asyncSupported = true),可以直接注册到 Servlet 容器中。

传输层维护了一个 ConcurrentHashMap<String, McpStreamableServerSession> sessions,以 Mcp-Session-Id 为 key 管理所有活跃会话。它的 doPost 方法是整个 MCP 协议交互的入口,处理逻辑分为几个分支:

首先是 initialize 请求——这是唯一不需要 Mcp-Session-Id 头的请求。Servlet 调用 sessionFactory.startSession(initializeRequest) 创建新会话,将会话 ID 通过 Mcp-Session-Id 响应头返回给客户端。此后该会话就存入 sessions 映射表,等待后续请求。

对于非 initialize 请求,Servlet 校验 Mcp-Session-Id 头并查找会话。如果是 JSON-RPC Request(如 tools/call),Servlet 会开启一个 SSE 流——设置 response.setContentType("text/event-stream"),调用 request.startAsync() 开启异步上下文,然后通过 session.responseStream(jsonrpcRequest, sessionTransport) 执行注册的请求处理器,将结果以 SSE event 的形式写回。

这个设计的精妙之处在于:一个 HTTP POST 请求既可以是普通的 JSON 响应(initialize 阶段),也可以是 SSE 流式响应(tool 调用阶段),完全由消息类型决定。对于 AI Agent 来说,只需用标准 HTTP 客户端就能完成所有 MCP 交互。

doGet 方法用于开启长生命周期的监听流——客户端通过 GET 请求打开一条 SSE 通道,服务器后续的主动通知(如 notifications/tools/list_changed)和服务器发起的请求(如 sampling/createMessage)都通过这条通道推送。doDelete 方法用于终止会话。

3.5 McpServerFeatures:工具注册的函数式 API

SDK 使用 record 类型定义工具规格。同步版本的 SyncToolSpecification 接受一个 BiFunction<McpSyncServerExchange, CallToolRequest, CallToolResult> 作为 handler——这是一个标准的函数式接口:

public record SyncToolSpecification(
    McpSchema.Tool tool,
    BiFunction<McpSyncServerExchange, McpSchema.CallToolRequest, McpSchema.CallToolResult> callHandler) {
}

jaws 在 JawsMcpServer.buildSyncToolSpecifications() 中构建的正是这个 record,handler 用 lambda 表达式 (exchange, request) -> JawsMcpToolHandler.handleToolCall(spec, request) 实现。McpSyncServerExchange 是每次请求的上下文对象,提供客户端信息、会话 ID、传输层上下文,以及服务器→客户端的反向调用能力(如 createMessage 请求模型生成、listRoots 列出客户端根目录)。

3.6 McpSchema:纯 record 的 JSON-RPC 世界

McpSchema 是一个 6400 多行的 final 类,内部定义了 MCP 协议涉及的所有数据结构,全部使用 Java record 实现。核心类型包括:

JSONRPCRequest 是 JSON-RPC 请求的 record,有 jsonrpcmethodidparams 四个字段。MCP 比 JSON-RPC 2.0 更严格——请求 ID 必须存在且只能是 String 或 Integer/Long,不允许 null ID。

Tool record 定义了 MCP 工具:name(工具名)、description(描述)、inputSchema(输入参数的 JSON Schema,类型为 Map<String, Object>)、outputSchema(可选的输出 Schema,2025-06-18 协议版本引入)、annotations(工具注解,含 readOnlyHintdestructiveHintidempotentHint 等提示布尔值)。jaws 在构建 Tool 时设置了 name、description 和 inputSchema,未设置 outputSchema 和 annotations。

CallToolResult 是工具调用的返回值,包含 contentList<Content>,内容列表)、isError(是否为错误)、structuredContent(结构化输出,2025-06-18 引入)。Content 是一个多态接口,子类型包括 TextContentImageContentAudioContentEmbeddedResourceResourceLink。jaws 的 JawsMcpToolHandler 使用 CallToolResult.builder().addTextContent(text).isError(false).build() 构建成功结果,将 RPC 返回值序列化为文本内容。

3.7 协议版本协商

SDK 支持四个协议版本:2024-11-05(初始版本)、2025-03-26(引入 Streamable HTTP)、2025-06-18(引入 Tool outputSchema 和 structuredContent)、2025-11-25(最新)。协商逻辑在 McpAsyncServer 的 initialize 请求处理器中:服务器默认返回支持的最高版本,但如果客户端请求的版本在支持列表中,则回显客户端版本。这保证了向前兼容——老客户端可以继续用老协议版本通信。

3.8 会话生命周期与 Mcp-Session-Id

Streamable HTTP 模式下,会话 ID 是整个交互的核心纽带。完整生命周期如下:

客户端 POST 一个 initialize 请求(不带 session ID),服务器创建 McpStreamableServerSession,生成 UUID 作为会话 ID,通过 Mcp-Session-Id 响应头返回。客户端后续所有请求都必须携带这个头。服务器通过 sessions.get(sessionId) 查找会话,找不到则返回 404。

客户端可以通过 GET 请求打开监听流——服务器在 McpStreamableServerSession.listeningStream() 中存储 SSE 传输引用,后续所有服务器主动推送都通过这条通道。也可以通过 DELETE 请求显式终止会话。

SDK 还支持 keepAliveInterval 配置——定期向所有活跃会话发送 ping,防止连接因超时被代理或防火墙断开。这在长时间没有 tool 调用的 AI 对话场景中特别有用。

3.9 一次 tools/call 请求的完整链路

将上面所有组件串联起来,当 AI Agent 调用一个 MCP Tool 时的完整流程如下:

客户端 POST 到 /mcp,带 Mcp-Session-Id 头,body 是 {"jsonrpc":"2.0","method":"tools/call","id":42,"params":{"name":"DemoService_hello","arguments":{"arg0":"world"}}}

Servlet 的 doPost 反序列化消息,识别为 JSONRPCRequest(非 initialize),查找会话,开启 SSE 流,调用 session.responseStream(jsonrpcRequest, sessionTransport)

McpStreamableServerSession 查找 requestHandlers.get("tools/call")——这个 handler 是 McpAsyncServerprepareRequestHandlers() 中注册的 toolsCallRequestHandler()。它将 params 反序列化为 CallToolRequest,在 this.tools 列表中查找名为 DemoService_hello 的工具,校验输入参数,然后调用 toolSpecification.callHandler().apply(exchange, callToolRequest)

这个 handler 正是 jaws 注册的 lambda (exchange, request) -> JawsMcpToolHandler.handleToolCall(spec, request)。它将 MCP 请求转化为 Jaws RPC 调用,拿到结果后构建 CallToolResult 返回。

SDK 将结果映射为 JSONRPCResponse.result(request.id(), result),通过 SSE 流写出一帧 event: message\ndata: <jsonrpcResponse>,然后关闭 per-request transport,Servlet 的 AsyncContext 完成,HTTP 响应结束。

这条链路穿越了 SDK 的每一层:Servlet 传输层 → 流式会话 → 请求处理器映射 → McpAsyncServerExchange → 工具 handler → JSON SPI 序列化 → McpSchema record。

四、架构设计:协议无关的桥接抽象

在直接看 MCP 或 REST 实现之前,有必要先理解 jaws 桥接架构的核心设计——一个协议无关的中间抽象层。这层抽象位于 jaws-core 模块中,包含三个关键类:ServiceMethodSpecArgumentConverterJsonSchemaGenerator。它们的职责是将"一个 Java 方法"转化为一种与传输协议无关的元数据描述,使得 MCP、REST 乃至未来的 GraphQL、gRPC 网关都能复用同一套逻辑。

4.1 ServiceMethodSpec:方法规格的统一表示

ServiceMethodSpec 是整个桥接架构的核心数据结构,它持有调用一个 RPC 方法所需的全部信息:

public class ServiceMethodSpec {

    /** 对外部 tool/action 名称,如 "DemoService_getUser" */
    private final String actionName;

    /** Jaws 服务接口全限定名 */
    private final String interfaceName;

    /** 方法名 */
    private final String methodName;

    /** 参数类型描述(Jaws parameterDesc 格式) */
    private final String[] parameterTypes;

    /** Java Method 对象,用于参数转换 */
    private final Method method;

    /** 能执行此方法的 Jaws Provider */
    private final Provider<?> provider;

    /** 方法参数,用于 Schema 生成 */
    private final Parameter[] parameters;
    // ...
}

这个设计的关键在于 provider 字段——它直接持有 Jaws 的 Provider<?> 引用,使得无论是 MCP Tool 调用还是 REST API 调用,最终都通过同一个 Provider.call() 方法执行,走的是完全一致的 RPC 调用链路。

actionName 的生成规则也值得注意:对于非重载方法,命名为 InterfaceName_methodName;对于重载方法,追加数字后缀(如 DemoService_save_1DemoService_save_2),确保每个方法有唯一的标识。

4.2 ArgumentConverter:参数类型转换

外部请求(MCP JSON 参数或 REST JSON body)需要转换为 Java 方法的实际参数。ArgumentConverter 处理这个过程:

public static Object[] convertArguments(Parameter[] parameters, Map<String, Object> arguments) {
    if (parameters.length == 0) {
        return new Object[0];
    }

    Object[] args = new Object[parameters.length];
    for (int i = 0; i < parameters.length; i++) {
        Parameter param = parameters[i];
        String paramName = param.isNamePresent() ? param.getName() : null;

        Object value = null;
        if (arguments != null) {
            if (paramName != null && arguments.containsKey(paramName)) {
                value = arguments.get(paramName);
            } else if (arguments.containsKey("arg" + i)) {
                value = arguments.get("arg" + i);
            } else if (arguments.size() == 1) {
                value = arguments.values().iterator().next();
            }
        }

        args[i] = convertArgument(value, param.getType());
    }
    return args;
}

参数匹配有三重策略:优先用参数名(需编译时加 -parameters),其次用 arg0/arg1 的位置约定,最后对单参数方法直接取唯一的值。类型转换覆盖了所有基本类型和包装类,复杂 POJO 则通过 fastjson2 做 JSON→对象的转换。

4.3 JsonSchemaGenerator:类型到 Schema 的自动推导

MCP 协议要求每个 Tool 声明 inputSchema,REST API 的服务详情端点也返回 Schema 信息。JsonSchemaGenerator 负责从 Java 类型自动推导 JSON Schema(draft 2020-12),支持基本类型、日期时间类型、枚举、数组、集合、Map 和复杂 POJO,并通过递归字段展开处理嵌套对象(最大深度 5 层):

private static Map<String, Object> generateObjectSchema(Class<?> clazz, int depth) {
    Map<String, Object> schema = new LinkedHashMap<>();
    schema.put("type", "object");

    Map<String, Object> properties = new LinkedHashMap<>();
    List<String> required = new ArrayList<>();

    Class<?> current = clazz;
    while (current != null && current != Object.class) {
        for (Field field : current.getDeclaredFields()) {
            if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) {
                continue;
            }
            Map<String, Object> fieldSchema = generateTypeSchema(field.getGenericType(), depth + 1);
            properties.put(field.getName(), fieldSchema);
            if (!field.getType().isPrimitive()) {
                required.add(field.getName());
            }
        }
        current = current.getSuperclass();
    }

    if (!properties.isEmpty()) {
        schema.put("properties", properties);
    }
    if (!required.isEmpty()) {
        schema.put("required", required);
    }
    return schema;
}

JsonSchemaGenerator 内部维护了一个静态映射表 PRIMITIVE_SCHEMAS,预定义了所有 Java 基本类型和包装类到 JSON Schema 类型的对应关系:boolean 映射为 "type": "boolean",int/long/short/byte 映射为 "type": "integer",float/double/BigDecimal/BigInteger 映射为 "type": "number",String/char 以及所有日期时间类型(Date、LocalDate、LocalDateTime、Instant 等)映射为 "type": "string"。枚举类型会自动提取所有枚举常量生成 "enum" 约束。集合类型(List、Set)映射为 "type": "array" 并递归推导泛型元素类型。Map 类型映射为 "type": "object" 并用 additionalProperties 描述值的 Schema。复杂 POJO 则通过 generateObjectSchema 递归展开所有字段,包括父类字段,最大递归深度限制为 5 层以防止循环引用导致的栈溢出。

注意它还会遍历父类字段,并自动将非基本类型字段标记为 required,确保 AI Agent 调用时能获得足够的参数约束信息。这套 Schema 生成逻辑对于 MCP 和 REST 是完全相同的——MCP 用它构建 Tool 的 inputSchema,REST 用它在方法详情端点返回参数结构信息,真正做到了一处生成、多处使用。

五、MCP 桥接深度解析

有了上面的基础抽象,MCP 桥接的实现就变得清晰了。整体调用链路如下:

AI Agent ──MCP (HTTP+SSE)──▶ jaws-mcp Servlet ──Jaws RPC──▶ Provider

5.1 JawsMcpServer:构建与注册

JawsMcpServer 是 MCP 桥接的核心类,采用 Builder 模式。它基于 MCP Java SDK 2.0.0,使用 HttpServletStreamableServerTransportProvider 作为传输层。核心流程是:扫描服务接口的所有 public 方法 → 为每个方法生成 ServiceMethodSpec → 由 spec 构建 McpServerFeatures.SyncToolSpecification → 注册到 McpSyncServer

public static List<McpServerFeatures.SyncToolSpecification> buildSyncToolSpecifications(
        List<ServiceMethodSpec> methodSpecs) {
    List<McpServerFeatures.SyncToolSpecification> specifications = new ArrayList<>();

    for (ServiceMethodSpec spec : methodSpecs) {
        Map<String, Object> inputSchema = JsonSchemaGenerator.generateMethodSchema(spec.getParameters());

        McpSchema.Tool tool = McpSchema.Tool.builder(spec.getActionName(), inputSchema)
                .description(buildMethodDescription(
                        getInterfaceClass(spec.getInterfaceName()),
                        spec.getMethod(),
                        spec.getParameterTypes()))
                .build();

        McpServerFeatures.SyncToolSpecification toolSpec = new McpServerFeatures.SyncToolSpecification(
                tool,
                (exchange, request) -> JawsMcpToolHandler.handleToolCall(spec, request)
        );

        specifications.add(toolSpec);
    }
    return specifications;
}

Tool 的描述信息格式为 DemoService.hello(String): String,这样 AI Agent 既能看到方法签名,也能看到返回类型,便于决定何时调用。

5.2 JawsMcpToolHandler:调用执行

当 AI Agent 调用一个 MCP Tool 时,JawsMcpToolHandler 负责将 MCP 请求转化为 Jaws RPC 调用,并将结果转回 MCP 响应:

public static McpSchema.CallToolResult handleToolCall(ServiceMethodSpec spec, McpSchema.CallToolRequest request) {
    try {
        Map<String, Object> arguments = request.arguments();

        // 构建 Jaws 请求
        DefaultRequest jawsRequest = new DefaultRequest();
        jawsRequest.setInterfaceName(spec.getInterfaceName());
        jawsRequest.setMethodName(spec.getMethodName());
        jawsRequest.setParametersDesc(buildParametersDesc(spec.getParameterTypes()));

        // 通过共享的 ArgumentConverter 转换参数
        Object[] args = ArgumentConverter.convertArguments(spec.getParameters(), arguments);
        jawsRequest.setArguments(args);

        // 调用 Provider
        Provider<?> provider = spec.getProvider();
        Response response = provider.call(jawsRequest);

        // 转换响应
        if (response instanceof DefaultResponse dr) {
            if (dr.getException() != null) {
                return createErrorResult(dr.getException());
            }
            return createSuccessResult(dr.getValue());
        }
        // ...
    } catch (Exception e) {
        log.error("[JawsMcp] Tool call failed: {}", spec.getActionName(), e);
        return createErrorResult(e);
    }
}

成功结果通过 CallToolResult.builder().addTextContent(text).isError(false).build() 返回,对象类型的结果会被 fastjson2 序列化为 JSON 字符串;异常则标记 isError(true) 返回错误信息。这种设计使得 AI Agent 既能读取基本类型返回值,也能解析复杂对象。

5.3 JawsMcpAutoConfiguration:Spring Boot 自动装配

MCP 桥接通过 jaws-mcp-spring-boot-starter 实现开箱即用。JawsMcpAutoConfiguration 的设计很有讲究——它在 ContextRefreshedEvent 时才动态注册 Tools,因为此时 Jaws 的服务导出(JawsBootstrap)已经完成,所有 ServiceBean 都已就绪:

@AutoConfiguration(after = JawsAutoConfiguration.class)
@EnableConfigurationProperties(JawsMcpProperties.class)
@ConditionalOnProperty(prefix = JawsMcpProperties.CONFIG_PREFIX, name = "enabled", 
    havingValue = "true", matchIfMissing = true)
public class JawsMcpAutoConfiguration {

    @Bean
    public ApplicationListener<ContextRefreshedEvent> jawsMcpToolRegistrar(
            ApplicationContext applicationContext, McpSyncServer mcpSyncServer) {
        return event -> {
            Map<String, ServiceBean> serviceBeans = applicationContext.getBeansOfType(ServiceBean.class);
            // ...
            for (ServiceBean serviceBean : serviceBeans.values()) {
                Class<?> interfaceClass = serviceBean.getInterface();
                String interfaceName = interfaceClass.getName();

                // 白黑名单过滤
                if (!includeSet.isEmpty() && !includeSet.contains(interfaceName)) continue;
                if (excludeSet.contains(interfaceName)) continue;

                Provider<?> provider = exporters.get(0).getProvider();
                List<ServiceMethodSpec> methodSpecs = JawsMcpServer.createMethodSpecs(interfaceClass, provider);
                List<McpServerFeatures.SyncToolSpecification> syncSpecs =
                        JawsMcpServer.buildSyncToolSpecifications(methodSpecs);

                for (McpServerFeatures.SyncToolSpecification spec : syncSpecs) {
                    mcpSyncServer.addTool(spec);
                    toolCount++;
                }
            }

            if (toolCount > 0) {
                mcpSyncServer.notifyToolsListChanged();
            }
        };
    }
}

注意 @AutoConfiguration(after = JawsAutoConfiguration.class) 确保在 Jaws 自动装配之后加载,而 ContextRefreshedEvent 监听器使用最低优先级,确保在服务导出完成后执行。注册完成后调用 notifyToolsListChanged() 通知 MCP 客户端 Tool 列表已更新——这是 MCP 协议的 tools/listChanged 通知机制。

5.4 配置与使用

只需引入依赖并添加配置即可启用 MCP 桥接:

<dependency>
    <groupId>org.hongxi</groupId>
    <artifactId>jaws-mcp-spring-boot-starter</artifactId>
    <version>${jaws.version}</version>
</dependency>
jaws:
  mcp:
    enabled: true
    server-name: jaws-mcp-demo
    server-version: 1.0.0
    endpoint: /mcp
    include-services:          # 白名单,空则暴露全部
      - org.hongxi.jaws.sample.api.DemoService
    exclude-services:          # 黑名单,优先级高于白名单
      - org.hongxi.jaws.sample.api.OrderService

5.5 示例服务与 curl 验证

示例服务 DemoService 涵盖了各种方法签名——基本类型参数、复杂对象参数、集合返回、Map 返回、方法重载、异步返回:

public interface DemoService {
    String hello(String name);
    User rename(User user, String name);
    List<User> getUsers();
    Map<String, User> map(List<User> users);
    void save(Contacts contacts);
    int save(List<Contacts> contactsList);   // 重载
    CompletableFuture<String> helloAsync(String name);
}

启动示例(需要 Nacos 在 127.0.0.1:8848 运行):

./mvnw spring-boot:run -pl jaws-samples/jaws-sample-provider-mcp

MCP 端点可用:http://localhost:8082/mcp。完整的 MCP 协议交互流程如下:

# 1. 初始化会话(响应头中返回 Mcp-Session-Id)
curl -s -D - -X POST http://localhost:8082/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}}'

# 2. 发送 initialized 通知
curl -s -X POST http://localhost:8082/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Session-Id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

# 3. 列出所有 Tools
curl -s -X POST http://localhost:8082/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Session-Id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

# 4. 调用 Tool
curl -s -X POST http://localhost:8082/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Session-Id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"DemoService_hello","arguments":{"arg0":"Jaws MCP"}}}'

tools/list 的响应中会包含所有自动注册的 Tool,以 DemoService_hello 为例:

{
  "name": "DemoService_hello",
  "description": "DemoService.hello(String): String",
  "inputSchema": {
    "type": "object",
    "properties": {"arg1836019240": {"type": "string"}},
    "required": ["arg1836019240"],
    "additionalProperties": false
  }
}

六、REST 桥接深度解析

如果说 MCP 桥接面向 AI Agent,那 REST 桥接面向的是传统 HTTP 客户端——无需理解 MCP 协议,通过简单的 HTTP/JSON 即可调用后端 RPC 服务。

6.1 RestInvokeServlet:轻量实现

RestInvokeServlet 是一个纯粹的 HttpServlet 实现,无 Spring MVC 依赖。它维护两个注册表:serviceRegistry(按接口名索引的方法列表)和 actionRegistry(按 actionName 索引的快速查找表)。核心的 handleInvoke 方法如下:

private void handleInvoke(String path, HttpServletRequest req, HttpServletResponse resp) throws IOException {
    String interfaceName = path.substring(0, lastSlash);
    String methodName = path.substring(lastSlash + 1);

    ServiceMethodSpec spec = findMethodSpec(interfaceName, methodName);
    if (spec == null) {
        sendError(resp, HttpServletResponse.SC_NOT_FOUND, "Method not found");
        return;
    }

    Map<String, Object> arguments = readRequestBody(req);
    Object[] args = ArgumentConverter.convertArguments(spec.getParameters(), arguments);

    DefaultRequest jawsRequest = new DefaultRequest();
    jawsRequest.setInterfaceName(spec.getInterfaceName());
    jawsRequest.setMethodName(spec.getMethodName());
    jawsRequest.setParametersDesc(buildParametersDesc(spec.getParameterTypes()));
    jawsRequest.setArguments(args);

    Provider<?> provider = spec.getProvider();
    Response response = provider.call(jawsRequest);

    Map<String, Object> result = new LinkedHashMap<>();
    result.put("actionName", spec.getActionName());
    if (response.getException() != null) {
        result.put("success", false);
        result.put("error", response.getException().getMessage());
        resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    } else {
        result.put("success", true);
        if (response instanceof DefaultResponse dr) {
            result.put("data", dr.getValue());
        }
        resp.setStatus(HttpServletResponse.SC_OK);
    }
    JSON.writeTo(resp.getOutputStream(), result);
}

可以看到,REST 和 MCP 的调用路径高度一致——都通过 ArgumentConverter 转换参数,都构建 DefaultRequest 调用 Provider.call(),差异只在于请求来源和响应格式。

6.2 三个 API 端点

REST 桥接提供三个端点,对应 RestInvokeServletdoGetdoPost 方法:

方法 路径 说明
GET /rest/services 列出所有已注册服务及其方法
GET /rest/services/{interfaceName} 查看指定服务的方法详情(含 JSON Schema)
POST /rest/invoke/{interfaceName}/{methodName} 调用指定方法,body 为 JSON 参数

GET 端点返回的方法详情中包含 inputSchema,这样调用方无需查阅文档就能知道参数结构:

# 列出所有服务
curl -s http://localhost:8083/rest/services

# 调用方法
curl -s -X POST http://localhost:8083/rest/invoke/org.hongxi.jaws.sample.api.DemoService/hello \
  -H "Content-Type: application/json" \
  -d '{"arg0": "Jaws REST"}'

响应:

{
  "actionName": "DemoService_hello",
  "interfaceName": "org.hongxi.jaws.sample.api.DemoService",
  "methodName": "hello",
  "success": true,
  "data": "Hello, Jaws REST"
}

6.3 JawsRestAutoConfiguration

REST 桥接的自动装配与 MCP 桥接如出一辙——同样的 @AutoConfiguration(after = JawsAutoConfiguration.class)、同样的 ContextRefreshedEvent 监听、同样的白黑名单过滤:

@Bean
public ApplicationListener<ContextRefreshedEvent> jawsRestServiceRegistrar(
        ApplicationContext applicationContext, RestInvokeServlet restInvokeServlet) {
    return event -> {
        Map<String, ServiceBean> serviceBeans = applicationContext.getBeansOfType(ServiceBean.class);
        for (ServiceBean serviceBean : serviceBeans.values()) {
            // 白黑名单过滤...
            Provider<?> provider = exporters.get(0).getProvider();
            restInvokeServlet.registerService(interfaceClass, provider);
            serviceCount++;
        }
    };
}

七、架构思考:协议无关的桥接抽象

回顾整个实现,最值得品味的设计是 jaws-core 中提取的三个共享组件。MCP 桥接和 REST 桥接在以下环节完全复用同一套逻辑:

方法注册:两者都调用 JawsMcpServer.createMethodSpecs() 生成 ServiceMethodSpec 列表,方法命名、重载处理的逻辑只有一份实现。

参数转换:两者都通过 ArgumentConverter.convertArguments() 将外部参数转为 Java 对象数组,基本类型、包装类、复杂 POJO 的转换逻辑统一管理。

Schema 生成:两者都通过 JsonSchemaGenerator.generateMethodSchema() 生成 JSON Schema,MCP 用它作为 Tool 的 inputSchema,REST 用它作为方法详情的返回信息。

RPC 调用:两者都构建 DefaultRequest 并调用 Provider.call(),走的是完全相同的 Jaws RPC 调用链路。

这意味着如果未来需要增加 GraphQL 网关或 gRPC 网关,只需新建一个 jaws-graphql 模块,编写 GraphQL 协议到 ServiceMethodSpec 的适配逻辑即可,核心的参数转换、Schema 生成、RPC 调用全部复用。这种"协议无关中间层 + 协议特定适配层"的架构,使得扩展新协议的成本降到最低。

从另一个角度看,ServiceMethodSpec 本质上是在做一种"接口级别的反射元数据封装"。它把 Java 方法拆解为 name、参数类型、参数对象、Provider 引用的组合,相当于一个轻量的"服务目录"。而 MCP 和 REST 只是这个目录的两种不同"视图"。

八、总结

jaws 的这次更新,核心思路是把传统 RPC 框架的服务能力以协议无关的方式桥接到 AI 生态和 HTTP 生态。MCP 桥接让 AI Agent 能通过标准 MCP 协议直接调用后端 RPC 服务,REST 桥接让传统 HTTP 客户端能通过简单的 JSON API 调用同样的服务。两者共享 ServiceMethodSpecArgumentConverterJsonSchemaGenerator 三个核心组件,实现了"一次注册,多协议暴露"。

从我的四项目版图来看,jaws 的 MCP 桥接补上了"后端服务 → AI Agent"这关键一环。如果说 whatsmars 聚焦 LangChain4j 原理、spacecloud 聚焦 Spring AI 应用、babi 聚焦 Agent 工程化,那 jaws 的 MCP 桥接解决的是"已有微服务资产如何被 AI Agent 复用"这个基础设施层面的问题。对于已有大量 RPC 服务的团队来说,引入 jaws-mcp-spring-boot-starter 就能让这些服务自动出现在 AI Agent 的工具列表中,零改造成本。

项目地址:https://github.com/javahongxi/jaws

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐