目录
一、最终的形态
1.1 项目结构
mcp-java-tutorial/
├── src/main/java/com/example/mcp/
│ │
│ ├── step1/CalculatorServer.java # 最小 MCP Server(无 LLM)
│ ├── step2/ManualClient.java # 最小 MCP Client(无 LLM)
│ ├── step3/McpToClaudeTool.java # 桥接层(30 行)
│ ├── step4/AgentMain.java # agent loop
│ │
│ ├── core/ # 通用基础设施
│ │ ├── McpAgent.java # 可复用的 agent loop
│ │ └── StdioLifecycle.java # 进程生命周期管理
│ │
│ └── db/ # 只读数据库 MCP
│ ├── Db.java # 数据源(唯一要改的层)
│ ├── SqlGuard.java # ★ 安全层
│ ├── AuditLog.java # 审计日志
│ ├── SchemaIntrospector.java # 表结构自省
│ ├── DbMcpServer.java # ★ MCP Server 主体(4 个工具)
│ ├── DbAgentMain.java # 接 LLM 的入口
│ ├── DbClientDemo.java # 验证:功能 + 攻击 + 误伤
│ └── ReadOnlyAccountDemo.java # 验证:只读账号
1.2 主流程
剥掉所有细节,整个项目的主线就是这一段:
// ① 起 MCP Client(子进程 + 初始化)
// 注:initialize() 属于「有握手」的那版规范。2026-07-28 已删除握手,
// 但官方 Java SDK 尚未跟进,所以这里仍要调——详见 mcp-02 的说明。
McpSyncClient mcp = McpClient.sync(new StdioClientTransport(serverParams, jsonMapper))
.build();
mcp.initialize();
// ② MCP 工具 → Claude 工具(30 行胶水)
List<Tool> claudeTools = mcp.listTools().tools().stream()
.map(McpToClaudeTool::toClaudeTool)
.toList();
// ③ agent loop
List<MessageParam> messages = new ArrayList<>();
messages.add(MessageParam.builder().role(USER).content(question).build());
for (int turn = 1; turn <= MAX_TURNS; turn++) {
Message response = claude.messages().create(params); // 发给模型
for (ContentBlock block : response.content()) {
if (!block.isToolUse()) continue;
ToolUseBlock toolUse = block.toolUse().orElseThrow();
Map<String, Object> args = toolUse._input().convert(Map.class);
// ④ 模型要调工具 → 通过 MCP 执行
McpSchema.CallToolResult r = mcp.callTool(
new McpSchema.CallToolRequest(toolUse.name(), args));
toolResults.add(...); // 包成 tool_result
}
if (toolResults.isEmpty()) break; // ⑤ 模型不再要工具 → 结束
messages.add(工具结果);
}
1.3 完整链路
┌──────────┐
│ 你 │ "9 月领用最多的备件是什么?"
└────┬─────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ Agent 层(core/McpAgent) │
│ · 维护对话历史 │
│ · 把 MCP 工具转成 Claude 工具格式 │
│ · 驱动 while 循环 │
└────┬───────────────────────────────────────────┬────────────┘
│ tool_use │ tool_result
▼ ▲
┌──────────┐ ┌──────┴──────┐
│ Claude │ │ 你的代码 │
└──────────┘ └──────┬──────┘
│ JSON-RPC (stdio)
▼
┌─────────────────────────────────────────────────────────────┐
│ MCP Server 层(db/DbMcpServer) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ ① SqlGuard SQL 白名单 / 强制 LIMIT │ │
│ ├───────────────────────────────────────────────────────┤ │
│ │ ② schema 自省 读表注释喂给模型 │ │
│ ├───────────────────────────────────────────────────────┤ │
│ │ ③ 只读连接 + 超时 │ │
│ ├───────────────────────────────────────────────────────┤ │
│ │ ④ AuditLog 记录每一次调用(含被拒绝的) │ │
│ └───────────────────────────────────────────────────────┘ │
└──────────────────────────┬──────────────────────────────────┘
▼
┌─────────────┐
│ 数据库 │
└─────────────┘
二、这一路换来了什么
回头看,四步各解决了一个具体的认知问题。把它们列出来,比列代码清单有用。
2.1 第一步:不接 LLM 的 MCP
解决的问题:把 MCP 和 function calling 的混淆彻底消除。
怎么解决的:写了一个没有任何大模型的 MCP 链路。当你亲眼看到一个纯程序调用 MCP 工具,这个概念就再也混不了了。
代价:多花了一天。如果直接接 LLM 上手,第一天就能跑通 demo,但对协议的理解会一直浮在表面。
这个代价值不值? 值。因为后面所有设计决策都建立在这个理解上。比如「桥接层为什么只有 30 行」这个问题,不理解正交关系就答不出来。
2.2 第二步:接上 LLM
解决的问题:理解 agent 的本质。
怎么解决的:手写 agent loop,不用任何框架。
代价:要处理六个容易做错的地方(toParam、refusal 判断、tool_result 合并、is_error、退出条件、maxTokens)。用框架的话这些都被封装了。
值不值? 值。因为这六个点在生产环境出问题时会变成很难查的 bug——比如「模型渐渐不并行调用工具了」这种,不手写过一遍根本想不到和消息结构有关。
2.3 第三步:真实业务
解决的问题:理解「工具描述和数据结构描述」才是效果的决定因素。
怎么解决的:做只读数据库查询,被迫面对「模型看不见你的库」这个现实。
代价:要设计一套贴近真实的测试数据(含一张 5000 行大表)。
值不值? 值。这一步让我意识到,写 MCP Server 的工作量里,相当大一部分是在写「给模型看的文档」——工具描述、参数描述、表注释。
2.4 第四步:安全
解决的问题:从「能跑」到「能放心跑」。
怎么解决的:三道墙 + 12 个验证用例。
代价:写了不少代码,而且其中一部分(SqlGuard 的很多细节)在正常使用中永远不会触发。
值不值? 值。而且这一步的价值不在于代码本身,在于思维方式的转变:从「怎么让它工作」变成「怎么让它不闯祸」。
三、离生产还差什么
这一节是诚实的部分。上面那套东西能跑、能验证,但它不是生产系统。
| 缺什么 | 影响 | 怎么补 |
|---|---|---|
| 连接池 | 每次查询新建连接,高并发下打爆数据库 | HikariCP |
| 结果缓存 | 相同查询重复执行 | 按 SQL 哈希缓存 30 秒 |
| 真正的 SQL 解析 | 手写分词器理论上有绕过路径 | JSqlParser 之类的 AST 解析 |
| 数据脱敏 | 模型可能查出手机号、身份证号 | 结果集过脱敏规则 |
| 行级权限 | 所有用户看到的数据一样 | SQL 外套 WHERE,或按用户切换账号 |
| 结果摘要 | 现在按 4000 字符硬截断 | 只回传摘要 + 让模型按需再查 |
| 多轮记忆 | 每次 ask() 从空历史开始 | 把 messages 提升为 agent 的字段 |
| 调用限流 | 模型可能疯狂查库 | 按会话限制每分钟查询数 |
3.1 最该先做的两个
连接池和结果缓存。
前者是稳定性问题——没有连接池,几个并发请求就能把数据库连接数耗尽。
后者是成本问题——LLM 应用里,同一次会话中重复查询同一份数据非常常见,缓存能省下可观的 token 和时间。
3.2 数据脱敏为什么重要
这条容易被忽略,但它是合规问题。
模型查出来的数据会进入对话历史,而对话历史会被发送到模型提供方。如果你的库里有人事、财务、客户数据,这就是实实在在的数据泄露面。
只读账号解决的是「不能被改」,不是「不能被看」。 这是两个问题。
四、我对 MCP 的三点判断
做完这一轮,说三个结论。都是个人判断,仅供参考。
判断一:MCP 的价值在「边界」上
如果你只有一个模型、一个进程、一套工具,那 MCP 带来的收益很小——直接函数调用更简单。
MCP 的价值出现在边界上:
- 工具和 AI 应用不同语言
- 工具由不同团队维护
- 需要换模型而不改工具
- 工具需要独立部署和升级
只要有一项会变,那层协议抽象就值了。一项都没有,那它确实是多余的一层。
所以在决定用不用 MCP 之前,先问自己:我这些边界,未来会不会变?
判断二:真正的难点不在协议,在「怎么描述」
协议本身很简单——JSON-RPC 2.0 加四个方法名。看完第一篇你就能理解个大概。
难的是让模型用对。 而这件事的绝大部分工作量,花在写「给模型看的文档」上:
- 工具描述(做什么 + 什么时候用)
- 参数描述(每个参数是什么含义)
- 表结构注释(业务含义,不是类型说明)
这些在传统开发里属于「文档工作」,可以敷衍。在 MCP 里它们是功能的一部分——写得不好,功能就是不可用。
判断三:安全要重新想一遍
传统 Web 安全的心智模型是「防外部攻击者」。MCP 引入了一个新角色:
不可信的输入源,从「用户输入」变成了:
· 用户输入
· 模型生成的参数
· 模型读到的数据内容(← 这个是新的)
第三种是全新的攻击面——攻击载荷藏在数据里,用户完全不知情。
这意味着「输入校验」这件事的边界要往前推:不能只校验用户输入,还要校验模型生成的每一个参数,以及考虑模型读到的内容可能带有指令。
而这个防御不能靠 prompt,只能靠「模型管不到的地方」。
五、常见误区
误区一:以为跑通 demo 就等于会了。
跑通一个 demo 和能上生产之间,隔着的不是更多代码,是更多问题的意识:连接池、脱敏、限流、可观测性、失败降级。这些在 demo 里都不存在。
误区二:把手写 SQL 解析器当成完备方案。
它挡误操作很好用,但挡攻击不能只靠它。必须配一个只读账号。 这一点在第五篇反复讲过,但值得再强调一次。
误区三:工具描述写得像 API 文档。
API 文档是给人看的,读者有上下文。工具描述是给模型看的,它只有这一句话。
所以描述里要写「什么时候用」,而不只是「这是什么」。
误区四:跳过第一步,直接接 LLM。
可以,但代价是理解会浮在表面。后面遇到「为什么消息结构会影响模型的调用模式」这类问题时,会缺少底层依据。
误区五:只读账号就觉得安全了。
只读防的是「改数据」,不防「看数据」也不防「拖垮数据库」。这三个是不同的问题,需要不同的手段。
六、自检清单
系列收尾,做一次整体自测:
协议层
- MCP 解决的核心问题是什么?
- MCP 和 function calling 的关系,能用一句话说清吗?
- Host / Client / Server 的角色划分?谁和谁是 1:1?
- MCP 底层协议是什么?为什么规范删掉了握手,而手上这个 SDK 还在调
initialize()?
实现层
- agent loop 的五个步骤?
-
tool_use和tool_result怎么对应?多结果为什么要合并? - 桥接层为什么只要 30 行?
- stdio 传输最容易踩的坑?
应用层
- 为什么
describe_table比run_query更重要? - 工具描述的写法公式?
- 怎么让模型少写错 SQL?
安全层
- 为什么 prompt 不算安全边界?
- 三道墙分别挡什么?哪个不可替代?
- SQL 掩码是为了解决什么?为什么等长?
- 为什么要记录被拒绝的请求?
附:延伸阅读
相关资料
| 资源 | 说明 |
|---|---|
| Model Context Protocol 官方文档 | 协议规范与各语言 SDK |
| modelcontextprotocol/servers | 官方参考实现,PostgreSQL server 那份值得对照本系列的安全设计看 |
| OAuth 2.0 for MCP | 想把 Server 部署到公网时必读的鉴权部分 |
| Building effective agents | 什么场景该用 agent、什么场景不该,判断框架很清晰 |
| The 12-Factor App | 第四节的「离生产还差什么」,很多项都能对应到这里的条目 |
关联笔记
- MCP 实战(一)~(五) —— 本系列正文
- RAG 实战(六):把散落的零件拼成完整项目 —— 同样是系列收尾,同样是「诚实盘一遍还差什么」
- 系统扩展:从 0 到 1000 万用户 —— 连接池、缓存、限流这些手段的通用版本
最后更新:2026-10-10