又有时间搞自己的大模型本地应用了,之前做了springboot + langchain4j + qwen3:4b + milvus做了一套聊天机器人,尝试了function-call等函数式调用,为了更智能,准备接触一下MCP;为后续学习agent打技术基础,再到skill,长路漫漫啊;
在检索网上大量资料的情况下,发现有些博主对mcp有拿Langchain4j做实现,但是大部分都是官网的demo case,启动一个应用,与实际需求还是有区别,比如我想通过聊天对话,调用mcp的生成订单功能;对应的实现大致扫描了一波,发现mcp也会用到@Tool注解,这个注解是Function-call的核心注解,有点迷瞪了,两者的区别与边界到底在什么地方,没办法,只能通过千问来总结一下:

MCP的三种角色

映射到具体的服务和使用的cherry-studio工具上是下面的关系:

也就是说,我们开发mcp服务,只需要开发MCP对应的接口好了!
MCP协议的两种传输方式
HTTP: 客户端请求一个SSE(Server-Sent Events)通道以从服务器接收事件,然后通过HTTP POST请求发送命令。这种方式适用于需要跨网络通信的场景,通常用于分布式系统或需要高并发的场景。
STDIO: 客户端可以将MCP服务器作为本地子进程运行,并通过标准输入/输出直接与其通信。这种方式适用于本地集成和命令行工具,适合简单的本地批处理任务。

到Langchain4j的github官方源码中找MCP的Example【链接】
开发写 Server,Cherry Studio 提供 Host + Client,三者通过 stdio协议 进行协作;
💡 如果你只为 Cherry Studio 开发,用 stdio;如果要被远程 Agent 调用,才用 HTTP。
官网的SDK命令行解析
java
McpTransport transport = new StdioMcpTransport.Builder()
.command(List.of("/usr/bin/npm", "exec", "@modelcontextprotocol/server-everything@0.6.2"))
.logEvents(true) // only if you want to see the traffic in the log
.build();

如何编写MCP Server的工作流程脚本【stdio协议】
编写一个 MCP Server 的启动脚本,核心目标是:
启动一个可执行程序,它能通过 stdin 接收 MCP 请求,通过 stdout 返回 MCP 响应(通常为 NDJSON 格式),并符合 MCP 协议规范。
下面提供几种常见语言/场景下的 标准启动脚本模板,你可以根据实际工具类型选择或修改。
- 从 stdin 读取 MCP 请求(每行一个 JSON-RPC 2.0 消息)
- 向 stdout 写入 MCP 响应或通知(同样每行一个 JSON)
- stderr 可用于日志(不影响协议)
- 长期运行,不退出(除非收到 shutdown 或 SIGTERM)
- 支持 initialize 和 shutdown 方法(MCP 必须实现)
python、nodejs、shell、npm等命令均可以被mcp启动用作工具完成任务;标准格式皆不一样;
但大致可以总结为以下几个流程:
- 准备一个可执行的程序,语言任选:Python / Node.js / Java / Go / Rust / Shell(推荐 Python/JS)
- 遵守MCP通信规则【见上面的4点要素】
- 加安全限制:限制文件/命令的操作范围,防止路径穿越
- 打包成可执行文件【jar包、py文件等格式】
- 在idea中配置调用:在 cherry.yaml(或其他支持 MCP 的 IDE)中注册:
yml
tools:
- name: my-tool
command: ["python3", "/path/to/your_script.py"]
# 或 ["node", "tool.js"]
# 或 ["java", "-jar", "tool.jar"]
一句话总结:"写个能读 stdin、写 stdout、懂 JSON-RPC 的脚本,告诉 IDE 怎么启动它。"
无需网络、无需接口、无需部署 ------ 这就是 MCP stdio 模式的魅力。
如何通过http的调用方式接到开发的地气
-
1.选择合适的web框架【SpringBoot WebFlux】
-
2.实现两个核心端点:

mcp的http方式不强制要求使用sse;但官方推荐流式/异步响应;若简单接口可使用POST/mcp
-
3.处理MCP协议逻辑(java示例)
java
// Controller 示例(Spring Boot WebFlux)
@RestController
public class McpController {
private final ObjectMapper mapper = new ObjectMapper();
// POST /mcp ------ 处理所有 MCP 调用
@PostMapping(path = "/mcp", consumes = "application/json")
public Mono<String> handleMcpRequest(@RequestBody String jsonRequest) {
try {
JsonNode req = mapper.readTree(jsonRequest);
String method = req.path("method").asText("");
if ("initialize".equals(method)) {
return Mono.just(buildInitializeResponse(req.get("id")));
} else if ("file/read".equals(method)) {
return Mono.just(handleFileRead(req));
} else if ("shutdown".equals(method)) {
System.exit(0); // 或优雅关闭
return Mono.empty();
}
return Mono.error(new RuntimeException("Method not supported"));
} catch (Exception e) {
return Mono.just(buildErrorResponse(jsonRequest, e));
}
}
// GET /events ------ SSE 通道(可选,用于推送通知)
@GetMapping(path = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> events() {
return Flux.interval(Duration.ofSeconds(30))
.map(seq -> ServerSentEvent.<String>builder()
.event("keep-alive")
.data("{\"jsonrpc\":\"2.0\",\"method\":\"$/keepAlive\"}")
.build());
// 实际可推送 tool notification 等
}
// 工具方法:读文件(带安全校验)
private String handleFileRead(JsonNode req) throws IOException {
String uri = req.path("params").path("uri").asText();
Path root = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
Path target = root.resolve(uri).normalize();
if (!target.startsWith(root)) {
throw new SecurityException("Path outside root");
}
String content = Files.readString(target, StandardCharsets.UTF_8);
ObjectNode result = mapper.createObjectNode();
result.put("content", content);
return buildJsonRpcResponse(req.get("id"), result);
}
// 辅助方法:构建标准 JSON-RPC 响应
private String buildJsonRpcResponse(JsonNode id, JsonNode result) {
ObjectNode resp = mapper.createObjectNode();
resp.put("jsonrpc", "2.0");
resp.set("id", id);
resp.set("result", result);
return resp.toString();
}
}
- 4.Client如何对MCP-Server接口做调用
虽然只开发Server,但需要知道Client的行为
shell
# 1. 初始化
POST /mcp
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}
2. 调用工具
POST /mcp
{"jsonrpc":"2.0","id":2,"method":"file/read","params":{"uri":"README.md"}}
3. (可选)监听事件
GET /events → 接收 SSE 流
📌 Cherry Studio 默认不使用 HTTP 模式,但自定义 Agent 或 Web 应用可通过此方式集成。
- 5.启动&测试
http的方式
shell
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
配置client(非cherry-studio)的方式【cherry-studio只能stdio协议】
yml
# 假设你的 Agent 支持 HTTP MCP
mcp_servers:
- name: remote-file-reader
url: http://localhost:8080
如果想在cherry-studio中调用mcp的http协议接口的话,拿pyhton做个桥接的脚本

如何通过java开发stdio直接接到开发的心坎里
重申一下为什么提出这个问题;上面两个点提到过mcp的工具接口开发,但是有两种协议的格式:stdio和http;
- 其中上面第一个case,是通过stdio协议直接调起server端的命令来进行工作;
- 第二个case,是通过开发http接口,来被client或者agent进行调用
- 本次的case,开发java程序,用stdio协议来实现mcp,被cherry-studio配置和调用
为什么我如此执着于cherry-studio,是因为它可以把我们的mcp接口做成一个功能,通过聊天对话的形式启用;先模仿,再深究;
1、编写MCP-Server【stdio协议】
java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class McpFileReader {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final Path ROOT_DIR = Paths.get("").toAbsolutePath().normalize();
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line;
// 持续读取 stdin(每行一个 JSON-RPC 请求)
while ((line = reader.readLine()) != null) {
try {
JsonNode req = MAPPER.readTree(line.trim());
JsonNode resp = handleRequest(req);
if (resp != null) {
System.out.println(resp.toString());
System.out.flush(); // ⚠️ 必须 flush,否则 IDE 收不到
}
} catch (Exception e) {
sendError(line, -32603, e.getMessage());
}
}
}
private static JsonNode handleRequest(JsonNode req) throws IOException {
String method = req.path("method").asText("");
switch (method) {
case "initialize":
return buildInitializeResponse(req.get("id"));
case "file/read":
return handleFileRead(req);
case "shutdown":
System.exit(0);
default:
sendError(req.toString(), -32601, "Method not found: " + method);
return null;
}
}
private static JsonNode buildInitializeResponse(JsonNode id) {
ObjectNode tool = MAPPER.createObjectNode()
.put("name", "file/read")
.put("description", "Read a UTF-8 text file")
.set("inputSchema", MAPPER.createObjectNode()
.put("type", "object")
.set("properties", MAPPER.createObjectNode()
.set("uri", MAPPER.createObjectNode()
.put("type", "string")
.put("description", "Relative file path")))
.putArray("required").add("uri"));
ObjectNode result = MAPPER.createObjectNode();
result.putObject("capabilities").putArray("tools").add(tool);
return createJsonRpcResponse(id, result);
}
private static JsonNode handleFileRead(JsonNode req) throws IOException {
String uri = req.path("params").path("uri").asText(null);
if (uri == null || uri.isEmpty()) {
throw new IllegalArgumentException("Missing 'uri' parameter");
}
// 🔒 安全校验:防止路径穿越(如 ../../../etc/passwd)
Path target = ROOT_DIR.resolve(uri).normalize();
if (!target.startsWith(ROOT_DIR)) {
throw new SecurityException("Access denied: path outside project root");
}
if (!Files.exists(target)) {
throw new FileNotFoundException("File not found: " + uri);
}
if (!Files.isRegularFile(target)) {
throw new IllegalArgumentException("Not a regular file: " + uri);
}
String content = Files.readString(target);
ObjectNode result = MAPPER.createObjectNode().put("content", content);
return createJsonRpcResponse(req.get("id"), result);
}
private static JsonNode createJsonRpcResponse(JsonNode id, JsonNode result) {
ObjectNode resp = MAPPER.createObjectNode();
resp.put("jsonrpc", "2.0");
resp.set("id", id);
resp.set("result", result);
return resp;
}
private static void sendError(String raw, int code, String message) {
try {
JsonNode req = MAPPER.readTree(raw);
JsonNode id = req.path("id");
ObjectNode err = MAPPER.createObjectNode()
.put("jsonrpc", "2.0")
.set("id", id.isNull() ? MAPPER.nullNode() : id)
.putObject("error")
.put("code", code)
.put("message", message);
System.out.println(err.toString());
System.out.flush();
} catch (Exception ignored) {
System.err.println("Error response failed: " + message);
}
}
}
2.构建可执行JAR(maven)
pom
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>mcp-file-reader</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>McpFileReader</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
3.打包构建jar
shell
mvn clean package
4.准备测试文件
在项目根目录创建一个测试文件:
shell
echo "# Hello from MCP!" > test.md
4.配置cherry-studio
编辑项目根目录下的cherry.yaml
yml
tools:
- name: java-file-reader
command: ["java", "-jar", "/full/path/to/target/mcp-file-reader-1.0.jar"]
5.测试
方式一:手动命令行测试
shell
# 启动 Java MCP Server(它会等待输入)
java -jar target/mcp-file-reader-1.0.jar
# 在另一个终端发送请求
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | java -jar target/mcp-file-reader-1.0.jar
echo '{"jsonrpc":"2.0","id":2,"method":"file/read","params":{"uri":"test.md"}}' | java -jar target/mcp-file-reader-1.0.jar
如果返回以下数据,代表正常:
json
{"jsonrpc":"2.0","id":2,"result":{"content":"# Hello from MCP!\n"}}
方式二:再cherry-studio中直接使用
- 重启 Cherry Studio(确保加载新工具)
- 在聊天中提示 AI:"请读取 test.md 文件内容"
- AI 应自动调用你的 file/read 工具并返回内容
6.关键点回顾

再回顾MCP
通过上面执行的例子来看,好像mcp,也没什么特殊,只是用stdio和http,做接口功能;
但是这种功能接口,目前看起来,是完全独立一个的命令行或者工具包,没办法用AI做推理,只能被动的执行命令;唯一的用处就是被AI用来做可插拔的、结构化的来使用外部的工具;只是起到一个扩展AI边界的功能,相当于游戏的外挂一样;
瞬间脑子蹦出了几个字:【工具即服务】让AI,能干活儿了;不再是叨叨叨的吐字儿了!
如果按照这个定位来看,拿java开发mcp-server将不再是一个好的选择,必须python【短小精悍】!
再回到function-call,就可以完美的比较两者的区别:
Function-Call: 大模型内置的推理能力的补充,属于LLM API的扩展参数【LLM自带的】
MCP:去中心化的工具生态协议,工具自主运行、自主声明能力,AI想用它,可以付费【外挂,可插拔到不同LLM】
🎯 MCP 不是要取代 Function Calling,而是为"本地、安全、可共享"的工具场景提供更优解。
🚀 MCP 让 Agent 的"手脚"(工具)变得可移植、可组合、可共享。
总结
之前对mcp理解停留表面,被各种概率和理论吞没,只有实践是检验真理的唯一标准,这下子,值了;可以更好的往agent方向,出发了!