多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MCP 服务端开发:把 Java 能力安全开放给 AI Agent

MCP 服务端开发:把 Java 能力安全开放给 AI Agent MCP 服务端开发把 Java 能力安全开放给 AI Agent摘要本文以 Java 生态为背景系统阐述了如何构建一个安全可落地的 MCP Server。核心围绕 Tools、Resources、Prompts 三类能力的工程化设计展开强调从业务用例出发而非机械搬运内部 API给出 Java 分层架构与依赖方向。在此基础上深入探讨 stdio 与 Streamable HTTP 的选型、基于 Spring AI/SDK 的窄 Tool 注册、Resource 的授权与搜索、Prompt 模板的安全约束、HTTP OAuth 与业务权限的分层实现以及高风险操作的确认、幂等与审计。同时覆盖异常模型、超时与限流、配置版本管理、测试矩阵和性能指标最后归纳常见误区与面试表达要点帮助开发者把协议标准化能力转化为确定性的业务安全保障。企业把 AI Agent 接入内部系统时最容易走向两个极端一种是让模型直接拼 HTTP 请求把地址、认证头和历史 DTO 写进 Prompt另一种是做一个“万能工具”接收 URL、SQL 或脚本由模型自由执行。前者脆弱且难以治理后者则把模型的一次误判放大为真实系统操作。Model Context ProtocolMCP提供标准化的能力发现与调用接口使客户端能够了解服务端有哪些工具、上下文资源和提示模板。不过标准化协议不等于自动获得业务安全。MCP 的 HTTP 授权规范可以提供基于 OAuth 的传输层授权框架但资源级权限、租户隔离、高风险审批、幂等和业务审计仍然是服务端实现责任。本文以 Java 与 Spring 体系为背景从能力设计、分层实现、身份传递、输入输出约束到测试和运维给出一套可落地的 MCP Server 工程方案。一、先理解 MCP Server 的三类核心能力服务端最常见的能力是 Tools、Resources 和 Prompts。它们不是同一种 RPC 的三个名字而是控制权与用途不同的接口。能力主要用途控制方式例子Tools执行动作或计算支持类型化输入输出模型可以根据上下文选择调用查询判题状态、创建工单、冻结库存Resources以 URI 暴露应用管理的上下文数据应用选择读取和注入规则文档、题目说明、只读状态快照Prompts服务端提供可参数化的提示模板用户选择或触发故障复盘模板、代码评审模板“模型控制”不意味着模型拥有最终权限只表示客户端可以让模型决定是否调用 Tool。服务端仍要认证调用方、校验参数和执行授权。“应用控制”也不意味着 Resource 可以公开读取它仍需按身份过滤。“用户控制”的 Prompt 是可发现模板不应偷偷执行有副作用的操作。协议还包含能力协商、通知、日志、补全等机制。服务端在初始化阶段声明自己真正支持的能力客户端据此使用而不是假设所有 MCP Server 都具有全部特性。不要声明未完整实现的 capability否则会出现“协商成功、调用却总是失败”的兼容问题。二、从业务能力出发而不是原样搬运内部 API一个好的 Tool 应该窄、可解释、输入有界、输出稳定。例如活动和 OJ 系统可以提供 activity.get_detail、activity.check_stock、activity.reserve_stock、judge.get_submission。它们直接表达业务意图并能针对对象做授权。不建议提供 execute_sql、request_url、invoke_service 或 run_shell 等通用能力。它们把数据库、内网和主机攻击面交给模型无法做细粒度资源授权也很难审计“为什么发生这次修改”。即使工具描述写着“只查询当前租户”服务端也必须从可信身份取得租户并追加查询条件模型可能忽略提示Prompt 也可能受恶意文本影响安全控制只能依赖确定性代码。Tool 粒度也不能过细。把“查询订单、判断状态、创建退款、发送通知”拆成四个底层工具会让模型承担本应由应用服务保证的业务顺序。更适合暴露 refund.request 这一业务命令由 Java 应用服务完成状态校验、幂等、事务和事件发布。协议能力是应用用例的入口不是领域对象的 CRUD 映射。三、Java 分层与依赖方向MCP 协议对象不应进入领域核心。可以把系统分成四层MCP Transport / Session | MCP Handler协议适配、Schema、结果包装 | Application Service用例编排、事务、幂等、审批 | Domain / Gateway规则与外部依赖 | 数据库、HTTP 客户端、消息队列Handler 解析协议参数、取得调用上下文、调用应用服务再把稳定 DTO 转成 MCP 内容或结构化结果。应用服务不知道请求来自 MCP、REST 还是后台任务。这样未来升级 Java SDK、从 stdio 切换到 Streamable HTTP 时无需修改领域规则。publicrecordGetSubmissionQuery(longsubmissionId){}publicrecordSubmissionView(longsubmissionId,Stringstatus,IntegertimeMs,IntegermemoryKb,InstantupdatedAt){}publicinterfaceSubmissionQueryService{SubmissionViewget(GetSubmissionQueryquery,CallerContextcaller);}CallerContext 不由 Tool 参数构造而由传输层认证结果创建至少包含 subject、tenant、scopes、requestId 和必要授权属性。应用服务检查调用者能否查看该 submissionId 后再查询。Tool 入参里即使出现 tenantId也只能作为业务筛选值并与可信租户交叉验证不能覆盖 CallerContext。包结构可以使用 mcp.handler、application、domain、infrastructure。MCP SDK 依赖只出现在 mcp 模块ArchUnit 测试禁止 domain 和 application 依赖协议或 Spring Web 类型。四、选择 stdio 还是 Streamable HTTP本地桌面客户端或受控子进程通常适合 stdio客户端启动 Java 进程通过标准输入输出交换协议消息。它部署简单没有开放网络端口凭据从启动环境、受控配置或操作系统密钥存储传入。stdio 不采用 HTTP OAuth 授权流但不能因此把云端密钥硬编码在普通配置或命令参数中。远程、多用户和集中部署适合 Streamable HTTP。它支持网络访问、连接管理和标准 HTTP 安全设施也需要 TLS、反向代理、Host/Origin 校验、限流和授权配置。旧式 HTTPSSE 可能仍存在于历史客户端新增系统应根据当前客户端兼容矩阵选择规范推荐传输不要无需求地维护三套入口。场景推荐主要风险单机开发工具、客户端拉起进程stdio环境凭据、进程权限、stdout 污染企业共享服务、多租户访问Streamable HTTP网络暴露、OAuth、会话与限流历史客户端兼容有期限保留旧传输双协议维护、行为差异stdio 服务不能把普通日志写到 stdout否则会污染协议帧日志应写 stderr 或独立日志系统。HTTP 服务则要限制请求体和响应体大小设置读取、执行与空闲超时并正确处理客户端断开。五、用 Java SDK 注册窄 ToolJava MCP SDK 提供同步和异步 API并支持纯 Java 传输较新的生态中Spring 相关 WebMVC/WebFlux 传输由 Spring AI 提供。具体类名会随 SDK 版本演进项目应通过 BOM 固定兼容版本以锁定版本的官方示例为准。下面代码重点表达处理边界McpServerFeatures.SyncToolSpecificationgetSubmissionTool(SubmissionQueryServiceservice,CallerContextResolvercontexts){Stringschema { type: object, properties: { submissionId: { type: integer, minimum: 1 } }, required: [submissionId], additionalProperties: false } ;returnToolSpecifications.sync(judge.get_submission,查询当前调用者有权查看的判题结果,schema,(exchange,request)-{CallerContextcallercontexts.from(exchange);longidStrictArguments.of(request.arguments()).requiredLong(submissionId);SubmissionViewviewservice.get(newGetSubmissionQuery(id),caller);returnToolResults.structured(view);});}示例中的 ToolSpecifications 和 ToolResults 可以是项目对具体 SDK 的薄封装。JSON Schema 是第一道输入约束不是全部校验。服务端仍需防数值溢出、过长字符串、Unicode 规范化问题、非法枚举组合和业务越权。additionalProperties 设为 false 可以尽早发现拼错字段也减少模型意外传入敏感参数。输出采用小而稳定的 DTO为集合长度、文本长度和嵌套深度设上限。不要直接返回 JPA 实体、Feign 响应或异常堆栈。Tool 描述、Schema 和实际输出应同步可以用契约快照测试发现无意变更。六、Resources用 URI 提供受控上下文Resources 适合提供应用维护的只读上下文例如 oj://problems/1001/statement、policy://judge/runtime-limits/java 或 runbook://services/judge-worker。URI 是能力命名与寻址方式不是绕过授权的直链。ResourceContentreadProblem(Stringuri,CallerContextcaller){ProblemResourceKeykeyresourceUris.parseProblem(uri);authorization.requireProblemReadable(caller.subject(),caller.tenantId(),key.problemId());ProblemStatementstatementproblemQuery.getPublished(key.problemId());Stringmarkdownrenderer.renderForAgent(statement);if(markdown.length()limits.maxResourceChars()){thrownewResourceTooLargeException(uri);}returnResourceContent.text(uri,text/markdown,markdown);}不要把任意文件路径或任意 URL 映射成 Resource。file、http 通用读取器容易带来目录穿越、SSRF 和云元数据泄露。若要暴露文档库应使用内部资源 ID 到受控存储对象的映射校验规范化路径限制 MIME 类型、体积和读取时间。资源列表本身也会泄露信息。用户无权读取工单时listResources 不应返回标题和 URI。海量资源不要一次列出应使用资源模板、搜索或分页并把权限过滤下推到数据库而不是取出全部后内存过滤。七、Prompts模板不是隐藏指令后门Prompt 能力可以提供“生成故障复盘”“按团队规范审查代码”等参数化模板。模板参数需要 Schema 和长度限制渲染结果应允许用户检查。不要在模板中嵌入密钥、内部系统指令或绕过审批的暗示。prompt: incident.review arguments: service: judge-worker timeRange: 2026-07-01T10:00:00Z/2026-07-01T10:30:00Z rendered intent: 汇总指定时段告警、变更、影响和恢复证据 对缺失数据标注“未确认”不得臆测根因。Prompt 内容应有版本和负责人。修改模板可能改变模型行为应像代码一样评审和回归。用户参数始终是不可信文本模板中的防注入提示只能降低风险不能替代 Tool 权限。有副作用的操作仍由 Tool Handler 与应用服务的确定性校验控制。模板若引用 Resource应保持来源可追踪。在审计信息中保留资源 URI、版本或摘要哈希便于复现模型为何得出某个结论。八、HTTP 授权与业务权限必须分层MCP 的 HTTP 授权规范提供基于 OAuth 的授权框架包括受保护资源元数据、授权服务器发现和访问令牌使用。它解决客户端如何获得并向 MCP Server 呈现访问凭据。实现时遵循当前协议版本和所用 SDK 的授权扩展不要自创 query 参数 token。但有效访问令牌只完成第一步。服务端仍必须根据令牌建立 subject 与 tenant不信任模型传入的 userId校验 scope 是否允许调用某类 Tool校验当前主体是否能访问具体 submissionId、orderId高风险写操作检查角色、金额上限、审批状态和环境记录可关联真实身份与 Agent 会话的审计事件。publicReservationreserve(ReserveCommandcommand,CallerContextcaller){scopeAuthorizer.require(caller,inventory:reserve);tenantGuard.requireSameTenant(caller.tenantId(),command.activityId());policy.requireReservableBy(caller.subject(),command.activityId());if(command.quantity()approvalThreshold){approvalService.requireApproved(caller.subject(),command.approvalId(),command.fingerprint());}returnreservationService.reserve(command,caller.idempotencyKey());}客户端凭据与最终用户身份也要区分。企业 MCP Client 可能以应用身份连接却代表不同用户调用。若系统只看到共享服务账号资源审计会失真。优先使用能表达授权主体的机制无法传递用户身份时至少限制为低风险只读能力或建立受签名的受控委托上下文。stdio 的凭据来自受控环境不经过 HTTP OAuth 流。仍要最小权限、定期轮换并避免子进程继承无关环境变量。长期 Token 不能放入 Agent 可读取的普通 Resource。九、高风险 Tool 需要确认、幂等与审计删除数据、退款、发布配置等操作不应只依赖模型一句自然语言判断。可以采用“计划与执行分离”prepare 返回规范化操作摘要、风险和短期一次性 actionToken用户或审批系统确认后execute 携带令牌执行。refund.prepare - 返回 actionId、订单、金额、原因、影响、expiresAt、fingerprint 人工或策略审批 - 绑定 actionId 与 fingerprint refund.execute - 再次校验 身份、权限、审批、过期时间、参数指纹、幂等键、订单当前状态actionToken 要绑定具体参数与调用者防止审批后替换金额或订单。执行前重新检查资源状态并用数据库条件更新推进状态机。幂等键由稳定业务操作或客户端请求生成在数据库设唯一索引模型重试同一 Tool 时返回第一次结果而不是产生第二次效果。审计日志至少包括 requestId、sessionId、subject、tenant、toolName、toolVersion、参数摘要、资源 ID、授权决策、审批 ID、结果码、耗时和幂等命中。敏感参数使用哈希或脱敏摘要不能为了审计把密码和完整业务数据落日志。审计存储本身也要访问控制和防篡改。十、异常模型对 Agent 可行动对运维可定位把 Java 堆栈塞进 Tool 结果既泄露内部信息也不能帮助 Agent 正确恢复。建议定义稳定错误分类类别示例客户端可采取动作INVALID_ARGUMENTsubmissionId 非正数修正参数不重试原值NOT_FOUND资源不存在或不可见停止猜测向用户确认PERMISSION_DENIEDscope 或权限不足请求授权不尝试绕过CONFLICT当前状态不允许操作重新读取状态RATE_LIMITED超过配额在 retryAfter 后重试DEPENDENCY_UNAVAILABLE下游短暂故障有界退避INTERNAL未分类故障停止循环并提供 requestId对客户端返回安全消息、类别、是否可重试、retryAfter 和 requestId详细堆栈只进服务日志。不要把数据库“无行”一律映射为 NOT_FOUND因为权限过滤也可能产生无行。为避免资源枚举某些场景对外统一表现为不可见内部审计记录真实授权原因。重试受总步骤、截止时间和预算限制。写 Tool 网络超时属于“不确定结果”再次调用必须使用相同幂等键。服务端应能按该键查询最终状态而不是只返回模糊的“请重试”。十一、超时、并发、限流与输出大小Agent 可能并发尝试多个工具也可能在错误循环中重复调用。服务端需要按主体、租户、Tool 和资源设置限流对昂贵 Tool 限制并发。总超时沿调用链传播不能 MCP Handler 允许 60 秒、内部 HTTP 各自重试三次最终占满线程池。查询 Tool 可设置较短超时和有限重试写 Tool 谨慎自动重试并依赖幂等。耗时任务适合返回 operationId再通过只读工具查询进度而不是让一个连接长期占线程。取消到达时尽量停止下游工作但业务一致性仍由状态机保证。响应设置硬上限。数据库列表先分页日志只返回时间窗摘要文件通过 Resource URI 引用而不是内嵌几十兆文本。返回过大会增加序列化耗时、模型 Token 成本与提示注入面。推荐同时提供 machine-readable 字段和精简摘要。十二、配置与版本管理项目应固定 MCP SDK、Spring AI 和 Spring Boot 兼容版本不使用动态版本。较新的 Java SDK 把 Spring WebMVC/WebFlux 传输放在 Spring AI 生态核心 SDK 提供框架无关传输。选择一套并通过 BOM 管理不要混用不同代际示例。dependencyManagementdependenciesdependencygroupIdio.modelcontextprotocol.sdk/groupIdartifactIdmcp-bom/artifactIdversion${mcp.version}/versiontypepom/typescopeimport/scope/dependency/dependencies/dependencyManagementTool 名、Schema 和语义也需要版本策略。可兼容地增加可选输出字段通常比修改字段含义安全删除参数或改变单位应发布新 Tool 名或明确版本。服务端声明实现版本审计记录 toolVersion客户端升级前跑契约测试。密钥来自环境或密钥管理系统开发示例只给占位符。生产关闭详细错误响应配置允许 Host、Origin、请求体上限和受信代理。不同环境使用独立客户端注册与 scope避免测试 Agent 误连生产。十三、测试矩阵协议正确只是起点单元测试覆盖参数解析、业务授权、错误翻译和输出裁剪协议集成测试使用真实 MCP Client 与 Server 完成初始化、能力发现、Tool 调用、Resource 读取和 Prompt 获取安全测试验证未授权、跨租户、伪造 userId、超大参数、未知字段、路径穿越、SSRF 和重复写。TestvoidtoolCannotReadAnotherTenantsSubmission(){CallerContextcallerfixture.caller(tenant-a,user-1);longotherfixture.submission(tenant-b);assertThrows(PermissionDenied.class,()-queryService.get(newGetSubmissionQuery(other),caller));}TestvoidrepeatedReserveReturnsSameBusinessResult(){IdempotencyKeykeynewIdempotencyKey(req-20260719-001);Reservationfirsttool.reserve(command,caller.with(key));Reservationsecondtool.reserve(command,caller.with(key));assertEquals(first.id(),second.id());assertEquals(1,repository.countByIdempotencyKey(key));}还要测试客户端断开、下游超时、授权元数据不可用、令牌过期、并发限流、优雅关闭和协议版本不兼容。stdio 测试验证 stdout 只有协议内容HTTP 测试验证可信代理、Host/Origin 检查和请求大小限制。Prompt 注入测试应把恶意文本分别放入 Resource、Tool 输出和用户参数验证它无法越过服务端权限也不能取得未授权数据。目标不是证明模型永不受骗而是证明模型受骗后仍没有越权能力。十四、性能与成本指标指标至少分协议、业务和下游三层。协议层观察活跃会话、初始化失败、能力调用量、请求与响应大小业务层观察每个 Tool 的成功率、错误分类、幂等命中、授权拒绝和审批次数下游层观察数据库、HTTP、缓存和队列延迟。按 Tool 记录 P50、P95、P99但不要把用户 ID 和资源 ID 作为时序标签。对 Agent 成本还应记录响应字符数或估算 Token、无效重复调用次数和被裁剪结果数。查询即使只耗时 50 毫秒返回十万字也可能是昂贵且危险的设计。容量测试使用可复现的混合分布资源读取、缓存命中、慢下游、权限拒绝和写幂等逐步增加并发观察线程池、事件循环、连接池、GC、限流和下游放大。本文不虚构吞吐数字实际阈值由部署规格、传输方式和业务依赖共同测定。十五、常见误区第一把 MCP 当作“给 HTTP 接口换个壳”。如果 Tool 仍暴露旧 DTO 和万能路径协议标准化没有解决业务耦合。第二认为 MCP 完全没有授权或反过来认为启用 OAuth 后自动完成所有安全。准确边界是HTTP 传输可使用规范授权框架业务资源授权、租户隔离、审批与审计仍由实现负责stdio 不走这套 HTTP 流程。第三从 Tool 参数读取 userId、tenantId 作为可信身份。第四给模型任意 SQL、URL、文件路径或脚本执行器。第五只限制 Prompt不在服务端执行权限校验。第六高风险写操作没有审批和幂等客户端超时重试造成重复效果。第七把完整实体、日志和堆栈返回模型导致数据泄露与 Token 浪费。第八在 stdout 输出普通日志破坏 stdio。第九不固定 SDK 版本复制不同版本示例后依赖冲突。第十只做成功调用测试没有跨租户、超时、断线、重复调用和恶意 Resource 用例。十六、延伸与方案表达面试中可以先区分Tool 是模型可选择调用的类型化动作Resource 是应用控制的 URI 上下文Prompt 是用户选择的参数化模板。然后说明 MCP 负责发现、协商和调用标准化不替代业务服务的认证授权与审计。若被问如何开放退款能力可以回答不暴露通用 HTTP 工具定义 prepare 与 execute可信身份来自 HTTP 令牌或受控 stdio 环境Handler 只做协议适配应用服务校验 scope、租户、订单归属和状态审批绑定参数指纹唯一键保证幂等结果结构化且脱敏审计记录决策链最后用跨租户和重复执行测试验证。进一步可以讨论 stdio 与 Streamable HTTP 的取舍、Resource 列表泄露、Prompt 注入、不确定写结果对账、Schema 演进与限流维度。高质量回答的重点是把协议层能力和确定性业务控制分开。十七、总结可生产使用的 Java MCP Server核心不是注册多少工具而是为 Agent 建立一组窄、稳定、有权限边界的业务能力。Tools、Resources、Prompts 各有控制语义协议 Handler 与应用服务分层身份来自可信传输上下文输入通过 Schema 和业务规则双重校验输出被结构化、裁剪和脱敏写操作具备审批、幂等与审计。MCP 让能力发现与调用标准化HTTP 授权规范也提供了可互操作框架但最终能否安全上线仍取决于服务端是否执行资源级授权、租户隔离、错误边界、容量保护和故障验证。把这些责任放回确定性的 Java 代码Agent 才是在受控范围使用能力而不是获得通往内部系统的万能入口。参考资料MCP 官方 Server Concepts能力分类与控制语义MCP 官方 Authorization 规范HTTP 授权与受保护资源发现MCP Java SDK 官方文档同步/异步 Server、传输和能力注册Spring AI 官方 MCP 文档Spring Boot 集成与传输配置。版本与 API 会演进实际项目应以锁定版本的官方文档和兼容矩阵为准并在升级前运行协议契约与安全回归测试。
返回列表