AI应用-关于Function-Call和MCP的理解与应用

又有时间搞自己的大模型本地应用了,之前做了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启动用作工具完成任务;标准格式皆不一样;

但大致可以总结为以下几个流程:

  1. 准备一个可执行的程序,语言任选:Python / Node.js / Java / Go / Rust / Shell(推荐 Python/JS)
  2. 遵守MCP通信规则【见上面的4点要素】
  3. 加安全限制:限制文件/命令的操作范围,防止路径穿越
  4. 打包成可执行文件【jar包、py文件等格式】
  5. 在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方向,出发了!

相关推荐
sheji34162 小时前
【开题答辩全过程】以 工业车辆维修APP设计与实现为例,包含答辩的问题和答案
java
虫小宝2 小时前
淘客系统的容灾演练与恢复:Java Chaos Monkey模拟节点故障下的服务降级与快速切换实践
java·开发语言
yxm26336690812 小时前
【洛谷压缩技术续集题解】
java·开发语言·算法
键盘帽子2 小时前
多线程情况下长连接中的session并发问题
java·开发语言·spring boot·spring·spring cloud
无名-CODING3 小时前
Spring事务管理完全指南:从零到精通(上)
java·数据库·spring
fengxin_rou3 小时前
【黑马点评实战篇|第一篇:基于Redis实现登录】
java·开发语言·数据库·redis·缓存
数智工坊3 小时前
【数据结构-栈】3.1栈的顺序存储-链式存储
java·开发语言·数据结构
短剑重铸之日3 小时前
《设计模式》第七篇:适配器模式
java·后端·设计模式·适配器模式
DFT计算杂谈3 小时前
VASP+Wannier90 计算位移电流和二次谐波SHG
java·服务器·前端·python·算法
多多*3 小时前
2月3日面试题整理 字节跳动后端开发相关
android·java·开发语言·网络·jvm·adb·c#