尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

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

发布时间:2026/9/26 16:10:24

资讯中心
01
ARTICLE

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

使用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 文档来写把边界条件、单位、默认值都写进去模型选错工具的概率会明显下降。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。