# mcp-proxy **Repository Path**: li-zhigang123/mcp-proxy ## Basic Information - **Project Name**: mcp-proxy - **Description**: 一个基于 Spring Boot 的 MCP(Model Context Protocol)代理/网关服务 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-14 - **Last Updated**: 2026-10-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: MCP ## README # mcp-proxy 一个基于 **Spring Boot** 的 MCP(Model Context Protocol)代理/网关服务。 它同时扮演两个角色: - **对下游**:它是一个 MCP 服务端,把 MCP 暴露给任意 MCP 客户端(Streamable HTTP / HTTP+SSE / stdio)。 - **对上游**:它是一个 MCP 客户端,去连接部署在别处的 MCP 服务端(Streamable HTTP / HTTP+SSE / stdio)。 它把多个上游服务端的 tools / resources / prompts 聚合起来,统一暴露给客户端,并按名字前缀把请求路由回对应的上游。 ``` MCP 客户端 ── Streamable HTTP / SSE / stdio ──▶ mcp-proxy ── Streamable HTTP / SSE / stdio ──▶ 上游 MCP 服务端 ``` HTTP 入口由 Spring Boot 内嵌的 Tomcat 提供,MCP 的 servlet 传输通过 `ServletContextInitializer` 挂载上去。 ## 模块划分 工程是 **Maven 多模块**,代码按职责拆开: | Maven 模块 | Java 包 | 职责 | | --- | --- | --- | | `spi` | `com.mcpproxy.bridge` | 边界契约:`McpProxyBackend` 接口 + `ExposedCapabilities` 快照 | | `core` | `com.mcpproxy.config` | 共用配置与校验;client / server / transport-file 都依赖它 | | `server` | `com.mcpproxy.server` | 代理服务端(出站引擎):连接上游、聚合能力、按「对外名字 / URI」转发 | | `client` | `com.mcpproxy.client` | 代理客户端(入站):接收下游 MCP 请求,把聚合能力注册进 SDK 服务端 | | `transport-file` | `com.mcpproxy.transport.file` | `McpProxyBackend` 的「文件 + HTTP 回调」实现(两个角色共用) | | `app` | `com.mcpproxy` | Spring Boot 应用与组合根,产出可执行 jar | 依赖方向(无环)——`client` 和 `server` **互不依赖**,只共同依赖 `spi` 与 `core`; `transport-file` 也只依赖 `spi`: ``` app ─┬─▶ client ──────────────┬─▶ spi │ └─▶ core ├─▶ server ──────────────┬─▶ spi │ └─▶ core └─▶ transport-file ─────┬─▶ spi └─▶ (mcp) ``` `McpProxyBackend` 是 client 与 server 之间的唯一边界。它有两个实现,由 `app` 按 `mcp.interop.mode`(见 [运行模式](#运行模式profile))选一个注入给 `client`: - **同进程**:直接用 `server`(`ProxyServer`),Java 方法调用; - **跨进程**:用 `transport-file`(`FileProxyBackend` / `FileRequestAgent`),请求走文件、响应走 HTTP 回调。 无论哪种实现,都只按「对客户端暴露的名字 / URI」寻址、只传 MCP schema 类型、不共享 Java 引用, 所以 `client` 模块本身完全不需要改动。 ## 两个角色怎么对话(文件 + HTTP 回调) `inproc` 之外,`engine` 和 `inbound` 是两个各自独立部署的角色,可以跨服务器: ``` client 进程(区域 A) engine 进程(区域 B) ┌───────────────────────────┐ ┌───────────────────────────┐ │ ProxyClient │ │ FileRequestAgent │ │ │ McpProxyBackend │ ① 写请求文件 │ │ 扫 inbox/ │ │ ▼ │ ───────────────▶ │ ▼ 解析 │ │ FileProxyBackend ─────────┼── inbox/.json ══▶ │ ProxyServer(引擎)──▶ 上游 │ │ ▲ │ (你的同步进程) │ │ │ │ └── HTTP 回调 ◀─────────┼────── ② POST 结果 ───────┼───┘ │ └───────────────────────────┘ └───────────────────────────┘ ``` - ① client 把请求**原子写入**自己这边的 `inbox/.json`(先写 `.tmp` 再改名)。两台机器之间的文件传递由**你自己的同步进程**负责。 - ② engine 读到后执行,把结果 **POST 回 client 的 `mcp.interop.callback-url`**(带 `Authorization: Bearer `)。 - engine 处理完把**请求**归档到 `processed/req-.json`、把**响应**归档到 `processed/resp-.json`(都留痕、不删除);投递失败进 `failed/`。`processed/`、`failed/` 属于各角色本机,**不需要同步**。 需要同步的只有两个目录:`inbox/`(A → B)和 `responses/`(B → A)。`responses/` 用于 **启动引导**——client 构建 MCP 服务端时自己的 HTTP 还没监听,所以**只有这一次能力快照**由 engine 写回文件;其余响应一律走 HTTP 回调。能力快照只在启动时取一次,上游能力变了要**重启** client。 关键配置对齐(见各 profile 文件): | 配置 | inbound(A) | engine(B) | | --- | --- | --- | | `mcp.interop.base-dir` | 共享/被同步的目录 | 同一个目录 | | `mcp.interop.callback-url` | `http://:/internal/mcp-callback` | 不用配 | | `server.address` | 跨机必须 `0.0.0.0`(默认 `127.0.0.1` 收不到回调) | 不监听 | | `mcp.interop.token` | 同一口令 | 同一口令 | | `mcp.interop.request-timeout` | 要大于一次同步往返 + 上游执行耗时 | — | ## 工具包(bundle) 同一个网关可以按**不同的工具组合**暴露多个入口,让不同客户端/智能体看到不同的工具集。 配置(`mcp.bundles`,为空时等价于一个全量的默认 bundle): ```yaml mcp: bundles: default: include: ["*"] coding: include: ["filesystem__*", "calculator__*"] exclude: ["filesystem__delete"] readonly: include: ["calculator__*"] default-bundle: default ``` - **匹配对象**:工具/提示词用对外名字(`<上游>__<原名>`),资源用 URI,模板用 `uriTemplate`。 - **include 为空 = 全量**;`exclude` 在 include 之后生效;模式支持 `*` 通配。 - **路由**:默认 bundle 用基础端点(`/mcp`、`/sse`、`/message`);其它 bundle 用 `/mcp/`、`/sse/`、`/message/`。 - **拦截**:不在 bundle 里的工具**根本不会注册到该入口**,所以 `tools/list` 看不到, `tools/call` 直接报 `Tool not found`。 - **stdio** 只能服务一个 bundle,固定用默认 bundle。 - 每个 bundle 是一个独立的 `McpAsyncServer`,因此 `tools/list` 天然按 bundle 隔离。 > 注意:bundle 是**入站侧(client)**对聚合视图的裁剪;出站引擎仍然持有全量路由。 > 跨进程文件模式下,`inbox/` 是信任边界,谁写文件谁能构造请求——真要防越权,engine 侧还需按 > profile/bundle 二次校验(当前版本未做)。 ## 关于协议版本 官方日期版本只有四个: | 版本 | 说明 | | --- | --- | | `2024-11-05` | 最初的版本 | | `2025-03-26` | 引入 Streamable HTTP | | `2025-06-18` | 2025 年 6 月修订版(即你要的那个「6 月版」) | | `2025-11-25` | 更新的修订版 | 代理默认同时支持这四个版本,并按 MCP 规范做协议协商: - 客户端请求的版本在支持列表里 → 原样返回该版本; - 否则 → 返回支持列表里最新的版本。 ## 构建 需要 JDK 21+。工程是 **Maven 多模块**,自带 `mvnw`(会使用父目录的本地仓库 `.m2repo`): ```bash ./mvnw package -DskipTests # 产物:app/target/mcp-proxy.jar(Spring Boot 可执行 jar) ``` 只想构建某个模块(连同它依赖的模块): ```bash ./mvnw -pl server -am package -DskipTests ``` ## 运行模式(profile) 配置按模式拆成清晰的三份,切换模式只用一个开关(`--spring.profiles.active`),默认 `inproc`: | profile | 配置文件 | 角色 | 典型部署 | | --- | --- | --- | --- | | `inproc`(默认) | [`application-inproc.yml`](app/src/main/resources/application-inproc.yml) | 同进程:连上游 + 对下游提供 MCP | 单机 / 一个进程 | | `engine` | [`application-engine.yml`](app/src/main/resources/application-engine.yml) | 只跑代理服务端(出站引擎):读请求文件、执行、HTTP 回调 | 区域 B(server) | | `inbound` | [`application-inbound.yml`](app/src/main/resources/application-inbound.yml) | 只跑代理客户端(入站):提供 MCP、写请求文件、收 HTTP 回调 | 区域 A(client) | [`application.yml`](app/src/main/resources/application.yml) 是三种模式共用的部分(实现名、协议版本、命名空间分隔符、日志等)。 ```bash # 默认 inproc java -jar app/target/mcp-proxy.jar # 出站引擎(区域 B) java -jar app/target/mcp-proxy.jar --spring.profiles.active=engine # 入站客户端(区域 A) java -jar app/target/mcp-proxy.jar --spring.profiles.active=inbound ``` 跨机部署时,`engine` 与 `inbound` 需要对齐的几项(都在各自的 profile 文件里):`mcp.interop.base-dir`(共享/被同步的目录)、`mcp.interop.callback-url`(inbound 侧,engine 要能访问到)、`mcp.interop.token`(两边同一个)、以及 `server.address`(inbound 要监听可达地址)。详见 [两个角色怎么对话](#两个角色怎么对话文件--http-回调)。 ## 运行 启动类是 `com.mcpproxy.McpProxyApplication`: ```java @SpringBootApplication @ConfigurationPropertiesScan public class McpProxyApplication { public static void main(String[] args) { SpringApplication.run(McpProxyApplication.class, args); } } ``` ```bash java -jar app/target/mcp-proxy.jar # 或 ./mvnw -pl app -am spring-boot:run ``` 命令行参数就是 Spring Boot 的标准参数,配置里的任何一项都能覆盖: ```bash java -jar app/target/mcp-proxy.jar --server.port=9090 java -jar app/target/mcp-proxy.jar --logging.level.com.mcpproxy=debug java -jar app/target/mcp-proxy.jar --mcp.upstreams[0].url=http://127.0.0.1:9000/mcp java -jar app/target/mcp-proxy.jar --spring.config.additional-location=file:./local.yml ``` > **注意**:默认打包后这些 `application*.yml` 都在 jar 内部,**不要在 jar 所在目录放同名文件就以为能覆盖**—— > 需要覆盖已打包配置时,用 `--spring.config.additional-location=file:...` 或直接改源码重新打包。 配置写错时启动会直接失败并给出可读原因(Spring Boot 标准的 `APPLICATION FAILED TO START`),例如: ``` *************************** APPLICATION FAILED TO START *************************** Description: Error creating bean with name 'mcp-com.mcpproxy.config.McpProxyProperties': Invalid mcp-proxy configuration: - upstream 'broken'.url is required for the streamable-http transport ``` ### 默认地址 | 传输 | 地址 | | --- | --- | | Streamable HTTP | `http://127.0.0.1:8080/mcp` | | HTTP+SSE | `http://127.0.0.1:8080/sse`(消息端点 `/message`) | | 健康检查 / 聚合状态 | `http://127.0.0.1:8080/healthz` | ## 配置说明 完整示例见 [`application.yml`](src/main/resources/application.yml)。 ### Spring Boot 部分 | 配置项 | 说明 | | --- | --- | | `server.address` / `server.port` | HTTP 监听地址与端口(默认 `127.0.0.1:8080`) | | `logging.level.*` | 日志级别 | ### `mcp.*` 部分 | 配置项 | 默认值 | 说明 | | --- | --- | --- | | `mcp.name` / `mcp.version` | `mcp-proxy` / `1.0.0` | 对客户端通告的实现名与版本 | | `mcp.instructions` | — | 初始化时下发的说明 | | `mcp.streamable-http` | `{enabled:true, endpoint:/mcp}` | Streamable HTTP 入站 | | `mcp.sse` | `{enabled:true, endpoint:/sse, message-endpoint:/message}` | 旧版 HTTP+SSE 入站 | | `mcp.stdio` | `false` | 额外用 stdin/stdout 提供 MCP | | `mcp.namespace-separator` | `__` | 上游名与工具名之间的分隔符 | | `mcp.protocol-versions` | 四个版本 | **顺序为「旧 → 新」,最后一个是首选版本** | | `mcp.resource-uri-strategy` | `auto` | `auto` / `passthrough` / `namespaced` | | `mcp.annotate-descriptions` | `true` | 在描述后追加 `(via <上游名>)` | | `mcp.fail-fast` | `false` | 任一上游连不上就启动失败 | | `mcp.bearer-token` | — | 设置后,入站 HTTP 必须带 `Authorization: Bearer ` | | `mcp.request-timeout` | `120s` | 下游请求超时 | | `mcp.upstream-request-timeout` | `60s` | 上游请求超时 | | `mcp.upstream-connect-timeout` | `15s` | 上游连接超时 | | `mcp.upstream-initialization-timeout` | `30s` | 上游 initialize 握手超时 | | `mcp.keep-alive-interval` | `30s` | SSE 保活间隔 | | `mcp.upstreams[]` | `[]` | 上游列表,见下 | 时长写法支持 `30s` / `5m` / `2h` / `PT30S` / 纯数字(毫秒),由 Spring Boot 原生转换。 > **`mcp.protocol-versions` 的顺序很重要。** SDK 约定列表 **旧版本在前、新版本在后**,并且 > 把**最后一个**当作「最新/首选」版本:对外出站连接会请求它,入站遇到不支持的版本时也回退到它。 > > 例如只想说 2025-06 版:`["2024-11-05", "2025-03-26", "2025-06-18"]`。 ### `mcp.upstreams[]` | 字段 | 适用传输 | 说明 | | --- | --- | --- | | `name` | 全部 | 必填,唯一;命名空间前缀,只能用字母数字和 `._-` | | `transport` | 全部 | `stdio` / `streamable-http` / `sse`(也接受 `http` 等别名) | | `enabled` | 全部 | 默认 `true`;`false` 则跳过 | | `required` | 全部 | `true` 时连不上就启动失败(覆盖 `mcp.fail-fast`) | | `command` / `args` / `env` | `stdio` | 要启动的子进程 | | `url` | `streamable-http` / `sse` | 上游地址;带路径时该路径即端点 | | `endpoint` / `sse-endpoint` | 远程 | 显式指定端点路径,覆盖 `url` 中的路径 | | `headers` | 远程 | 附加到每个请求的静态请求头(鉴权用) | | `request-timeout` / `connect-timeout` / `initialization-timeout` | 全部 | 覆盖全局设置 | ### 占位符与环境变量 配置值支持 Spring 的占位符语法: ```yaml mcp: bearer-token: ${MCP_PROXY_TOKEN:} upstreams: - name: remote transport: streamable-http url: https://example.com/mcp headers: Authorization: "Bearer ${REMOTE_TOKEN:}" ``` - 语法是 `${VAR}` 或 `${VAR:默认值}`(**注意是单冒号**,不是 `${VAR:-默认值}`)。 - 没写默认值、变量又没设置时,Spring 会把 `${VAR}` **原样保留**(不会报错), 对鉴权头来说相当于鉴权失败(fail closed)。 - YAML 注释里的占位符不会被解析(之前的版本会因为注释里的 `${...}` 而启动失败,现已修复)。 ## 聚合与路由规则 - **tools / prompts**:命名为 `<上游名><分隔符><原名>`。例如上游 `calculator` 的 `add` 暴露为 `calculator__add`。调用时按第一个分隔符切分即可无歧义地路由回去。 - **resources**:默认(`auto`)原样透传 URI;如果多个上游暴露了同一个 URI,后出现的那个 自动改用 `mcp-proxy://<上游名>/`,保证路由唯一。 `namespaced` 表示始终使用合成 URI;`passthrough` 表示始终透传(重复的后者会被遮蔽并告警)。 - **resource templates**:始终原样透传(合成 URI 无法再做模板匹配)。 - 工具结果 / prompt 消息里内嵌的资源 URI 会被同步改写成客户端看到的那个 URI, 保证客户端拿到 URI 后还能读回来。 - 上游通过 `notifications/*/list_changed` 通知变更时,代理会重新拉取并增量增删对客户端的通告。 ## 日志 日志配置见 [`src/main/resources/logback-spring.xml`](src/main/resources/logback-spring.xml)。 **所有日志都写到 stderr**,因为开启 stdio 传输时 stdout 要留给 MCP 的 JSON-RPC 流。 Spring Boot 的 banner 也已关闭(`spring.main.banner-mode: off`)。 ## stdio 模式 如果 `mcp.streamable-http.enabled` 和 `mcp.sse.enabled` 都为 `false`,只留 `mcp.stdio: true`, [`ProxyEnvironmentPostProcessor`](src/main/java/com/mcpproxy/ProxyEnvironmentPostProcessor.java) 会自动设置 `spring.main.web-application-type=none`,**连 Tomcat 都不启动**、不占用任何端口。 这一点很重要:否则一台机器上跑多个 stdio MCP 服务端会互相抢端口。 客户端关闭 stdin 时代理会自动退出(退出码 0),不会留下孤儿进程。 ## 已验证的功能 用本机 `18001` 端口的真实 MCP 服务端(`calculator-mcp-server`,Streamable HTTP)实测: | 场景 | 结果 | | --- | --- | | Spring Boot 启动 | Tomcat 10.1.50 监听 `server.port` 8080 ✅ | | 配置来源 | 按模式拆分的 `application*.yml` 的 `mcp.*`,无独立 JSON 文件 ✅ | | 启动类 main | 只有一行 `SpringApplication.run(McpProxyApplication.class, args)` ✅ | | 上游连接 | `http://127.0.0.1:18001/mcp`,协商到 `2025-11-25` | | 聚合 | 6 个工具,全部带 `calculator__` 前缀 | | 客户端请求 `2025-06-18` | 协商返回 `2025-06-18` | | `calculator__add {2,3}` / `calculator__subtract {10,4}` | `5.0` / `6.0` | | 旧版 SSE 入站 | `event: endpoint` + `event: message` 往返成功 | | stdio-only 模式 | 协商 `2025-06-18`,`1+2` 返回 `3.0`,**未启动 Tomcat**,退出码 0 | | 两上游(`--spring.config.additional-location` 覆盖) | 19 工具 / 4 prompt / 7 resource / 2 template,路由正确 | | 非法 `transport` 值 | Spring 启动失败:`Unknown transport 'bogus-transport'` | | 缺少 `url` | Spring 启动失败:`url is required for the streamable-http transport` | | 注释里的 `${SOME_TOKEN}` | 不再导致启动失败 ✅(之前的 bug) | | 离线构建 | `./mvnw -o package` 通过 ✅ | | 多模块拆分 | `spi` / `core` / `server` / `client` / `transport-file` / `app` 六个模块,`./mvnw -o package` 全绿,产物 `app/target/mcp-proxy.jar` ✅ | | 配置按模式拆分 | 默认 `inproc`;`--spring.profiles.active=engine` 只跑引擎(不监听端口),`=inbound` 只跑入站 ✅ | | 跨进程文件传输 | `inbound` 写文件 → `engine` 读文件执行 → HTTP 回调,`calculator__multiply {6,7}` 返回 `42.0` ✅ | | 请求/响应留痕 | 请求归档 `processed/req-.json`、响应归档 `processed/resp-.json`(不删除),投递失败进 `failed/` ✅ | | 关键链路日志 | 发起(client 收到)、处理(engine 文件层 + 转发上游)、回调(engine POST + client 收到)各打 INFO;入站 `initialize` / `*/list` 另有 Servlet Filter 记录 ✅ | | 工具包(bundle) | `default`=6 工具、`add-only`=1、`no-weather`=5;`/mcp/add-only` 调未注册工具返回 `Tool not found` ✅ | ## 已知限制 - **不代理 `resources/subscribe` / `unsubscribe`**,因此资源能力里 `subscribe` 通告为 `false`。 - **不代理 sampling / elicitation / roots**。代理向上游声明空客户端能力,因此上游不会向代理发起 这类请求。这些能力需要把下游会话与上游请求关联起来,当前版本未实现。 - **completion 只在启动时注册**。运行中新增的 prompt / resource 暂时没有补全,重启后正常。 - 命名空间前缀会让工具名变长;如果客户端有工具名长度限制,可用更短的分隔符。 - **`engine` / `inbound` 模式下能力快照只在启动时取一次**:上游能力变化后要**重启** `inbound`(刻意的, 省掉文件同步那套实时刷新)。 - **`engine` 模式不监听任何端口**(没有 `/healthz`),它是纯 worker:只读文件 + POST 回调。 - **跨机文件传递依赖你自己的同步进程**:需要同步 `inbox/`(A → B)和 `responses/`(B → A); `processed/req-*.json`(请求)、`processed/resp-*.json`(响应)、`failed/` 属于各角色本机,不需要同步。 ## 代码结构 ``` pom.xml Maven 父 POM(聚合 + 依赖版本管理) spi/src/main/java/com/mcpproxy/bridge/ 边界契约(按可拆成网络设计) ├── McpProxyBackend.java 出站引擎的接口;入站模块只依赖它 └── ExposedCapabilities.java 聚合能力的只读快照(纯 MCP schema) core/src/main/java/com/mcpproxy/config/ 共用配置 ├── McpProxyProperties.java @ConfigurationProperties("mcp") + 校验 ├── InteropMode.java inproc / engine / inbound ├── UpstreamConfig.java 单个上游的配置 ├── TransportKind.java stdio / streamable-http / sse ├── ResourceUriStrategy.java auto / passthrough / namespaced └── converter/ 枚举值的友好别名绑定(@ConfigurationPropertiesBinding) server/src/main/java/com/mcpproxy/server/ 【代理服务端】出站引擎:连上游、聚合、转发 ├── ProxyServer.java 实现 McpProxyBackend;持有聚合视图与刷新 ├── capability/ 聚合视图与路由表 │ ├── CapabilityRegistry.java 聚合规则 + 按对外名字 / URI 查表 │ ├── NameMapper.java 对外名字拼接 │ └── UriMapper.java 合成资源 URI ├── forward/ │ └── ProxyRequestHandlers.java 一次转发的实际动作(阻塞) └── upstream/ 上游连接 ├── UpstreamManager.java ├── UpstreamConnection.java ├── UpstreamConnectionException.java └── ConfigurableStdioClientTransport.java client/src/main/java/com/mcpproxy/client/ 【代理客户端】入站:接收下游 MCP 请求 ├── ProxyClient.java 对下游暴露的 MCP 服务端(Streamable HTTP / SSE / stdio) ├── transport/ 入站 stdio 传输的补丁 │ ├── ConfigurableStdioServerTransportProvider.java │ └── EofAwareInputStream.java └── http/ 入站 HTTP 组件 ├── FileCallbackController.java 接收 engine 的 HTTP 回调(文件模式) ├── FileCallbackHandler.java 回调入口接口(app 接到 FileProxyBackend) └── McpRequestLoggingFilter.java 记录 initialize / */list 的元请求日志 transport-file/src/main/java/com/mcpproxy/transport/file/ 文件 + HTTP 回调的边界实现 ├── FileTransportSettings.java 目录 / 回调地址 / 超时 / 口令 ├── AtomicFiles.java 先写 .tmp 再原子改名 ├── dto/ │ ├── ProxyRequest.java 请求信封 │ └── ProxyResponse.java 响应信封 ├── client/ │ └── FileProxyBackend.java client 侧:写请求文件、等 HTTP 回调 └── server/ └── FileRequestAgent.java engine 侧:扫 inbox、调引擎、HTTP 回调 app/src/main/java/com/mcpproxy/ Spring Boot 应用与组合根 ├── McpProxyApplication.java Spring Boot 启动类(main 只有一行 run) ├── boot/ 启动与装配 │ ├── McpProxyConfiguration.java Bean 装配:运行时 + MCP servlet 挂载 + filter 注册 │ ├── McpProxyRuntime.java 组合根:按 interop 模式装配两个角色 │ ├── ProxyEnvironmentPostProcessor.java 派生配置(stdio-only / inbound 时 Web 容器的取舍) │ └── ProxyStartupBanner.java 启动摘要(ApplicationRunner) └── http/HealthController.java /healthz 聚合状态 app/src/main/resources/ ├── application.yml 三种模式共用 ├── application-inproc.yml inproc(默认) ├── application-engine.yml engine(出站引擎) ├── application-inbound.yml inbound(入站客户端) ├── logback-spring.xml 日志(全部输出到 stderr) └── META-INF/spring.factories 注册 EnvironmentPostProcessor ``` 关键路径只有一条,读代码建议按这个顺序: 1. `boot/McpProxyConfiguration` —— 看 servlet 是怎么和入站服务端接上的(`setSessionFactory` 反向注入); 2. `client/ProxyClient` —— 入站模块怎么拉取能力快照、注册 spec,并把请求转给边界; 3. `server/ProxyServer` —— 出站引擎怎么按名字查表、转发、刷新聚合视图; 4. `server/capability/CapabilityRegistry` —— 看聚合规则与路由表; 5. `server/upstream/UpstreamConnection` —— 看上游连接与快照; 6. `transport-file` —— 看边界怎么被换成「文件 + HTTP 回调」(跨进程模式)。 ## 关于 Lombok 项目用 Lombok 精简样板代码: | 注解 | 用在哪 | | --- | --- | | `@Getter` / `@Setter` | `McpProxyProperties`、`UpstreamConfig` 等配置类(需要空值兜底的集合/嵌套字段仍手写 setter) | | `@Slf4j` | 所有打日志的类,生成 `private static final Logger log` | 配置见 [`lombok.config`](lombok.config)。Lombok 是 `provided` 依赖且在 `spring-boot-maven-plugin` 里显式排除,不会被打进可执行 jar。 > IDE 需要安装 Lombok 插件(IntelliJ IDEA 2020.3+ 已内置)才能正确识别生成的 getter/setter。