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 Protocol(MCP)提供标准化的能力发现与调用接口,使客户端能够了解服务端有哪些工具、上下文资源和提示模板。不过,标准化协议不等于自动获得业务安全。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 时,无需修改领域规则。

public record GetSubmissionQuery(long submissionId) {}

public record SubmissionView(
        long submissionId,
        String status,
        Integer timeMs,
        Integer memoryKb,
        Instant updatedAt) {}

public interface SubmissionQueryService {
    SubmissionView get(GetSubmissionQuery query, CallerContext caller);
}

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 校验、限流和授权配置。旧式 HTTP+SSE 可能仍存在于历史客户端,新增系统应根据当前客户端兼容矩阵选择规范推荐传输,不要无需求地维护三套入口。

场景 推荐 主要风险
单机开发工具、客户端拉起进程 stdio 环境凭据、进程权限、stdout 污染
企业共享服务、多租户访问 Streamable HTTP 网络暴露、OAuth、会话与限流
历史客户端兼容 有期限保留旧传输 双协议维护、行为差异

stdio 服务不能把普通日志写到 stdout,否则会污染协议帧;日志应写 stderr 或独立日志系统。HTTP 服务则要限制请求体和响应体大小,设置读取、执行与空闲超时,并正确处理客户端断开。

五、用 Java SDK 注册窄 Tool

Java MCP SDK 提供同步和异步 API,并支持纯 Java 传输;较新的生态中,Spring 相关 WebMVC/WebFlux 传输由 Spring AI 提供。具体类名会随 SDK 版本演进,项目应通过 BOM 固定兼容版本,以锁定版本的官方示例为准。下面代码重点表达处理边界:

McpServerFeatures.SyncToolSpecification getSubmissionTool(
        SubmissionQueryService service,
        CallerContextResolver contexts) {

    String schema = """
        {
          "type": "object",
          "properties": {
            "submissionId": {
              "type": "integer",
              "minimum": 1
            }
          },
          "required": ["submissionId"],
          "additionalProperties": false
        }
        """;

    return ToolSpecifications.sync(
            "judge.get_submission",
            "查询当前调用者有权查看的判题结果",
            schema,
            (exchange, request) -> {
                CallerContext caller = contexts.from(exchange);
                long id = StrictArguments.of(request.arguments())
                        .requiredLong("submissionId");
                SubmissionView view = service.get(
                        new GetSubmissionQuery(id), caller);
                return ToolResults.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 是能力命名与寻址方式,不是绕过授权的直链。

ResourceContent readProblem(String uri, CallerContext caller) {
    ProblemResourceKey key = resourceUris.parseProblem(uri);
    authorization.requireProblemReadable(
            caller.subject(), caller.tenantId(), key.problemId());

    ProblemStatement statement = problemQuery.getPublished(key.problemId());
    String markdown = renderer.renderForAgent(statement);
    if (markdown.length() > limits.maxResourceChars()) {
        throw new ResourceTooLargeException(uri);
    }
    return ResourceContent.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。

但有效访问令牌只完成第一步。服务端仍必须:

  1. 根据令牌建立 subject 与 tenant,不信任模型传入的 userId;
  2. 校验 scope 是否允许调用某类 Tool;
  3. 校验当前主体是否能访问具体 submissionId、orderId;
  4. 高风险写操作检查角色、金额上限、审批状态和环境;
  5. 记录可关联真实身份与 Agent 会话的审计事件。
public Reservation reserve(ReserveCommand command, CallerContext caller) {
    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());
    }
    return reservationService.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_ARGUMENT submissionId 非正数 修正参数,不重试原值
NOT_FOUND 资源不存在或不可见 停止猜测,向用户确认
PERMISSION_DENIED scope 或权限不足 请求授权,不尝试绕过
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 管理,不要混用不同代际示例。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.modelcontextprotocol.sdk</groupId>
            <artifactId>mcp-bom</artifactId>
            <version>${mcp.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Tool 名、Schema 和语义也需要版本策略。可兼容地增加可选输出字段通常比修改字段含义安全;删除参数或改变单位应发布新 Tool 名或明确版本。服务端声明实现版本,审计记录 toolVersion,客户端升级前跑契约测试。

密钥来自环境或密钥管理系统,开发示例只给占位符。生产关闭详细错误响应,配置允许 Host、Origin、请求体上限和受信代理。不同环境使用独立客户端注册与 scope,避免测试 Agent 误连生产。

十三、测试矩阵:协议正确只是起点

单元测试覆盖参数解析、业务授权、错误翻译和输出裁剪;协议集成测试使用真实 MCP Client 与 Server 完成初始化、能力发现、Tool 调用、Resource 读取和 Prompt 获取;安全测试验证未授权、跨租户、伪造 userId、超大参数、未知字段、路径穿越、SSRF 和重复写。

@Test
void toolCannotReadAnotherTenantsSubmission() {
    CallerContext caller = fixture.caller("tenant-a", "user-1");
    long other = fixture.submission("tenant-b");

    assertThrows(PermissionDenied.class,
            () -> queryService.get(
                    new GetSubmissionQuery(other), caller));
}

@Test
void repeatedReserveReturnsSameBusinessResult() {
    IdempotencyKey key = new IdempotencyKey("req-20260719-001");
    Reservation first = tool.reserve(command, caller.with(key));
    Reservation second = tool.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 会演进,实际项目应以锁定版本的官方文档和兼容矩阵为准,并在升级前运行协议契约与安全回归测试。

Logo

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

更多推荐