新闻详情

新闻详情

首页 / 资讯中心 / 详情

使用Java实现MCP(模型上下文协议)完整指南:从零搭建可调试的MCP Server

发布时间:2026/9/26 16:10:27来源:尧图网络
使用Java实现MCP(模型上下文协议)完整指南:从零搭建可调试的MCP Server
1. 为什么 Java 开发者需要自己写一个 MCP ServerMCPModel Context Protocol模型上下文协议是 Anthropic 提出的开放协议用来标准化大语言模型与外部数据源、工具、服务之间的连接方式。你可以把它理解成「AI 世界的 USB-C 接口」以前每接一个工具就要写一套私有适配现在只要实现 MCP 协议任何支持 MCP 的客户端都能直接调用你的服务。对 Java 开发者来说这件事的意义在于你手上那些用 Spring Boot 写的内部系统、用 JDBC 连的数据库、用定时任务跑的数据管道都可以通过一个 MCP Server 暴露给 AI 工具调用而不需要把业务逻辑重写成 Python。适合谁适合已经熟悉 Java 生态、想让自己服务被 AI 助手或编码工具直接调用的后端开发者。这篇指南聚焦第一次落地 MCP 的完整链路初始化、工具注册、请求处理、本地调试。我会给出可复制的 Maven 依赖、Server 骨架代码以及用标准输入输出跑通验证的步骤。协议版本参考 2024-11-05传输层先用最朴素的 Stdio因为它是本地调试成本最低的方式。需要提前说明的是MCP Server 本身不负责「调用大模型」它只负责把能力暴露出去。真正发起调用的是 MCP 客户端比如支持 MCP 的编码工具或对话工具。所以调试时我们要么自己写一个最小客户端要么借助现成工具来验证。2. 前置准备环境、依赖与 TaoToken 接入2.1 技术栈与目录结构环境要求很基础JDK 11 及以上、Maven 3.6。JSON 处理用 Jackson日志用 SLF4J。项目结构建议按传输层、协议层、服务层拆开后面加 HTTP/SSE 传输时不用大改mcp-java-demo/ ├── pom.xml └── src/main/java/com/example/mcp/ ├── protocol/ # JSON-RPC 消息模型 ├── transport/ # Stdio / HTTP 传输实现 ├── server/ # 方法路由、工具注册 └── demo/ # 本地调试入口2.2 Maven 依赖配置project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdmcp-java-demo/artifactId version1.0.0/version properties maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target jackson.version2.15.2/jackson.version /properties dependencies dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version${jackson.version}/version /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-simple/artifactId version2.0.7/version /dependency /dependencies /project2.3 用 TaoToken 准备一个可调用的模型入口MCP Server 写完后你需要一个能发起工具调用的客户端来验证。如果你暂时没有现成的 MCP 客户端可以先用 TaoToken 的模型对话能力做联调在官网注册后进入控制台创建 API Key然后在模型对话页面确认模型可用。接入地址统一用https://taotoken.net/apiKey 在控制台生成。控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Key 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite模型对话验证https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite注意MCP Server 与模型调用是两件事。Server 负责暴露工具模型负责决定调哪个工具。调试阶段可以分开验证先确认 Server 能正确响应 JSON-RPC再接入模型侧。3. 可复制配置协议层与传输层实现3.1 JSON-RPC 消息模型MCP 基于 JSON-RPC 2.0所以先把请求、响应、错误三个模型建好。字段名必须严格对齐协议jsonrpc固定为2.0。package com.example.mcp.protocol; import java.util.Map; public class McpRequest { private String jsonrpc 2.0; private String id; private String method; private MapString, Object params; public McpRequest() {} public McpRequest(String id, String method, MapString, Object params) { this.id id; this.method method; this.params params; } public String getJsonrpc() { return jsonrpc; } public String getId() { return id; } public void setId(String id) { this.id id; } public String getMethod() { return method; } public void setMethod(String method) { this.method method; } public MapString, Object getParams() { return params; } public void setParams(MapString, Object params) { this.params params; } }响应模型要能同时承载成功结果和错误对象错误码沿用 JSON-RPC 标准package com.example.mcp.protocol; public class McpResponse { private String jsonrpc 2.0; private String id; private Object result; private McpError error; public static McpResponse success(String id, Object result) { McpResponse r new McpResponse(); r.id id; r.result result; return r; } public static McpResponse error(String id, int code, String message) { McpResponse r new McpResponse(); r.id id; r.error new McpError(code, message); return r; } public String getJsonrpc() { return jsonrpc; } public String getId() { return id; } public Object getResult() { return result; } public McpError getError() { return error; } public static class McpError { private int code; private String message; public McpError() {} public McpError(int code, String message) { this.code code; this.message message; } public int getCode() { return code; } public String getMessage() { return message; } } }3.2 Stdio 传输层Stdio 传输的核心是「一行一条 JSON 消息」。读的时候按行读写的时候按行写并 flush否则客户端会一直等。package com.example.mcp.transport; import com.example.mcp.protocol.McpRequest; import com.example.mcp.protocol.McpResponse; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.*; public class StdioTransport { private final BufferedReader reader; private final PrintWriter writer; private final ObjectMapper mapper new ObjectMapper(); public StdioTransport() { this.reader new BufferedReader(new InputStreamReader(System.in)); this.writer new PrintWriter(new OutputStreamWriter(System.out), true); } public McpRequest readRequest() throws IOException { String line reader.readLine(); if (line null || line.isBlank()) return null; return mapper.readValue(line, McpRequest.class); } public void writeResponse(McpResponse response) throws IOException { writer.println(mapper.writeValueAsString(response)); writer.flush(); } }提示日志千万不要往 stdout 打否则会污染 JSON 消息流。所有调试日志走 stderr这是 Stdio 传输最容易踩的坑。4. 工具注册与请求处理链路4.1 Server 骨架与路由表Server 的核心是一张「方法名 → 处理函数」的路由表。MCP 的标准方法包括initialize、tools/list、tools/call我们先把这三个实现掉。package com.example.mcp.server; import com.example.mcp.protocol.McpRequest; import com.example.mcp.protocol.McpResponse; import com.example.mcp.transport.StdioTransport; import java.io.IOException; import java.util.*; import java.util.function.Function; public class McpServer { private final StdioTransport transport; private final MapString, FunctionMapString, Object, Object handlers new HashMap(); private final MapString, ToolDefinition tools new LinkedHashMap(); private volatile boolean running true; public McpServer(StdioTransport transport) { this.transport transport; handlers.put(initialize, this::handleInitialize); handlers.put(tools/list, this::handleToolsList); handlers.put(tools/call, this::handleToolsCall); registerBuiltinTools(); } private void registerBuiltinTools() { tools.put(calculate, new ToolDefinition( calculate, 执行基础数学运算, Map.of( type, object, properties, Map.of( operation, Map.of(type, string, enum, List.of(add, subtract, multiply, divide)), a, Map.of(type, number), b, Map.of(type, number) ), required, List.of(operation, a, b) ) )); } public void start() throws IOException { while (running) { McpRequest req transport.readRequest(); if (req null) break; McpResponse resp dispatch(req); transport.writeResponse(resp); } } private McpResponse dispatch(McpRequest req) { FunctionMapString, Object, Object handler handlers.get(req.getMethod()); if (handler null) { return McpResponse.error(req.getId(), -32601, Method not found: req.getMethod()); } try { Object result handler.apply(req.getParams() null ? Collections.emptyMap() : req.getParams()); return McpResponse.success(req.getId(), result); } catch (Exception e) { return McpResponse.error(req.getId(), -32603, e.getMessage()); } } }4.2 initialize 与 tools/list 处理initialize要返回协议版本、服务端能力和服务信息。tools/list返回工具清单每个工具带 JSON Schema 描述参数。private Object handleInitialize(MapString, Object params) { return Map.of( protocolVersion, 2024-11-05, capabilities, Map.of( tools, Map.of(listChanged, false) ), serverInfo, Map.of( name, java-mcp-demo, version, 1.0.0 ) ); } private Object handleToolsList(MapString, Object params) { ListMapString, Object list new ArrayList(); for (ToolDefinition def : tools.values()) { list.add(Map.of( name, def.name(), description, def.description(), inputSchema, def.schema() )); } return Map.of(tools, list); }4.3 tools/call 与具体工具实现tools/call根据name路由到具体实现返回值必须是content数组元素类型为text。private Object handleToolsCall(MapString, Object params) { String name (String) params.get(name); SuppressWarnings(unchecked) MapString, Object args (MapString, Object) params.get(arguments); if (!tools.containsKey(name)) { throw new IllegalArgumentException(Unknown tool: name); } String text switch (name) { case calculate - doCalculate(args); default - throw new IllegalArgumentException(No impl: name); }; return Map.of(content, List.of(Map.of(type, text, text, text))); } private String doCalculate(MapString, Object args) { String op (String) args.get(operation); double a ((Number) args.get(a)).doubleValue(); double b ((Number) args.get(b)).doubleValue(); double r switch (op) { case add - a b; case subtract - a - b; case multiply - a * b; case divide - { if (b 0) throw new ArithmeticException(divide by zero); yield a / b; } default - throw new IllegalArgumentException(bad op: op); }; return String.format(%.2f %s %.2f %.2f, a, op, b, r); } public record ToolDefinition(String name, String description, MapString, Object schema) {}5. 验证请求与成功结果5.1 编译并启动 Servermvn clean compile mvn exec:java -Dexec.mainClasscom.example.mcp.server.McpServer启动后进程会阻塞在readLine()等待 stdin 输入。此时在终端手动粘贴一条 JSON 请求回车后应立刻看到响应。5.2 三条验证请求第一条初始化握手{jsonrpc:2.0,id:1,method:initialize,params:{protocolVersion:2024-11-05,capabilities:{}}}预期返回serverInfo和capabilitiesid回显为1。第二条列出工具{jsonrpc:2.0,id:2,method:tools/list,params:{}}预期返回tools数组包含calculate及其inputSchema。第三条调用工具{jsonrpc:2.0,id:3,method:tools/call,params:{name:calculate,arguments:{operation:multiply,a:12.5,b:4.2}}}预期返回{jsonrpc:2.0,id:3,result:{content:[{type:text,text:12.50 multiply 4.20 52.50}]}}三条都通过说明初始化、工具注册、请求处理链路全部打通。接下来可以把 Server 配置到支持 MCP 的客户端里让模型自动决定何时调用calculate。如果你需要长期跑编码类 Agent 任务可以考虑用 Coding Plan 做额度规划https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite6. 本篇常见错误排查6.1 响应里混入日志导致解析失败最常见的现象是客户端报「Unexpected token」原因是你在System.out里打了调试日志。Stdio 传输下 stdout 是协议通道任何非 JSON 输出都会破坏消息流。把所有System.out.println换成System.err.println或者用 SLF4J 配置输出到 stderr。6.2 忘记 flush 导致客户端一直等待PrintWriter默认不自动 flush如果构造时没传true响应会卡在缓冲区里。检查new PrintWriter(writer, true)这个参数或者每次写完手动flush()。6.3 方法名大小写或路径写错MCP 的方法名是tools/list、tools/call不是tools.list或tool/call。路由表 key 必须完全一致否则会返回-32601 Method not found。建议把方法名抽成常量避免手写拼错。6.4 inputSchema 不符合 JSON Schema 规范tools/list返回的inputSchema必须是合法的 JSON Schema。常见错误是required写成了字符串而不是数组或者properties里漏了type。客户端在校验参数时会直接拒绝表现为工具「看得见但调不动」。6.5 参数类型强转异常JSON 里的数字反序列化后可能是Integer也可能是Double直接(Double) args.get(a)会抛ClassCastException。统一用((Number) args.get(a)).doubleValue()处理这是我在实际调试中踩过的坑。6.6 进程退出后连接断开Stdio 模式下 Server 生命周期跟随父进程。如果客户端关闭了 stdinreadLine()返回null循环退出Server 正常结束。如果你希望 Server 常驻需要改用 HTTP/SSE 传输Stdio 不适合做后台服务。排查顺序建议先确认 stdout 干净再确认 flush然后核对方法名最后检查 Schema 和类型转换。这五步能覆盖九成以上的首次接入问题。接入文档和协议细节可以参考https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite如果你用的是 Claude Code 这类支持 Anthropic 协议的工具接入配置可以参考https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-code-anthropicutm_campaignrewrite把 Server 跑通只是第一步。真正让 AI 用起来顺手的是工具描述写得够清楚、参数 Schema 够严谨、错误信息够具体。我通常会把每个工具的description当成给模型看的 API 文档来写把边界条件、单位、默认值都写进去模型选错工具的概率会明显下降。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

WorkBuddy 从安装到实战:Node.js、Git、.NET 环境配置与任务自动化入门 2026/9/26 20:07:19

WorkBuddy 从安装到实战:Node.js、Git、.NET 环境配置与任务自动化入门

1. 为什么我要花一个周末折腾 WorkBuddy 先说结论:WorkBuddy 这类工具,本质上是一个“把日常重复操作打包成可复用任务”的自动化助手。它能帮你把打开项目、拉代码、跑构建、发通知这一串动作,压缩成一次点击或者一句指令。适合谁&#xff1…

阅读更多 →
Work Agent深度解读:AI长程任务如何重塑自动化工作模式 2026/9/26 20:07:12

Work Agent深度解读:AI长程任务如何重塑自动化工作模式

AI能力的迭代,正在从单次问答交互走向持续自主执行。早期大模型只能完成单轮问答,用户提出问题,模型即时给出一段文本,交互随回答生成即终止。随后多轮对话能力落地,模型可以记住上下文,在一轮轮对话里持续…

阅读更多 →
AI日报从0到1:信息筛选与结构化写作方法论 2026/9/26 20:07:12

AI日报从0到1:信息筛选与结构化写作方法论

1. 一份AI日报的诞生逻辑每天早上八点半,我习惯性打开自己维护的AI日报文档,把过去24小时里散落在各个角落的信息碎片拼成一张完整的图。这件事我已经连续做了快两年,从最开始的手忙脚乱到现在的流程化操作,中间踩过的坑足够写一本…

阅读更多 →
Meta 推出 Muse:手机上说一句,AI 替你把网页上的事办完 2026/9/26 20:07:12

Meta 推出 Muse:手机上说一句,AI 替你把网页上的事办完

人在国外,用手机跟 AI 说一句"帮我订明晚的酒店",它自己开浏览器比价、填表,付款前再停下来问你确认。Meta 刚把这件事做成了产品。 9 月 8 日,Meta 发布个人 AI 代理 Muse。主入口是手机 App,也能在网页和 …

阅读更多 →
开放式代码评审实践:从理念到工具链的完整落地指南 2026/9/26 20:07:12

开放式代码评审实践:从理念到工具链的完整落地指南

代码评审这件事,我见过太多团队做得“假”。一说要做 Code Review,就拉个会议,或者让组长在合并前扫一眼,然后大家继续埋头写代码,评审记录形同虚设。我之前带项目组的时候也踩过这个坑,后来花了很长时间把…

阅读更多 →
如何学习 opencode 和 openclaw 源码:从 TaoToken 配置骨架切入的源码阅读路线 2026/9/26 20:07:06

如何学习 opencode 和 openclaw 源码:从 TaoToken 配置骨架切入的源码阅读路线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉