前言

本文将带你使用 ollama + mcp-go + cherry-studio 在本地实现并构建完整的一套流程。

在 MCP 的风吹了那么久之后,相信你已经看过不少介绍了,如果你还没有实际体验过 MCP 的魅力,又或者你想通过 Golang 构建一个 MCP 服务,那么就来看看吧。

准备

解释

首先快速白话解释一下什么是 MCP ?

在没有 MCP 的时候

  • 用户:大模型,帮我点个外卖
  • 大模型:好的,我将告诉你,点外卖的步骤是….但你得自己点,因为我没有“手”

当有了 MCP 之后

  • 用户:大模型,帮我点个外卖
  • 大模型:好的,鳄了 MCP 服务去帮我下单一杯咖啡
  • 鳄了 MCP 服务:收到,咖啡订单已提交

所以,其实你不用了解太多的细节,也能知道 MCP 是做什么用的。关键就是,大模型本身仅提供了对话的能力,而想要实际操作一些东西的时候,它无法触及,而此时外部系统通常的做法是提供一些 API 接口,让其他系统能够调用自己,从而提供服务。而 API 的问题在于每家都有自己的参数列表和返回结果,MCP 的优势是在于制定了协议统一了接入的规范,从而让各个系统都能轻松的被大模型调用。从宏观的角度看,就是给大模型装上了 “手” 让他能触及实际的业务场景。

目标

我网上看了很多有关 MCP 实现案例的文章,发现一个共同的问题,大部分都在提供了一个工具服务为加法,a+b = c 这样。但是这完全体现不出 MCP 的意义(我的大模型自己不会算吗?非得你 MCP 教我?[狗头])。所以我们这次的目标是让大模型可以直接操作你本地的文件系统,从而体会到 MCP 的魅力。

直接上代码

不搞哪些花里胡哨的东西,直接上代码,而且非常简单,一看就懂。

tools.go

首先定义两个方法,用于操作本地的目录文件

  • listFiles 用于查询目录下的所有文件
  • renameFile 由于重命名一个文件
    directoryPath 设置了仅允许操作的文件目录,防止大模型看到一些不该看的小视频
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)

const (
directoryPath = "/tmp/linkinstar"
)

func listFiles(directory string, extension string) ([]string, error) {
if strings.TrimSpace(directory) != directoryPath {
return nil, errors.New(fmt.Sprintf("我无法访问目录 %s", directory))
}

fmt.Println("Listing files in directory :", directory)

files, err := os.ReadDir(directory)
if err != nil {
return nil, err
}

var result []string
for _, file := range files {
if file.IsDir() {
continue
}
if extension == "" || strings.HasSuffix(file.Name(), extension) {
result = append(result, file.Name())
}
}

return result, nil
}

func renameFile(directory string, oldName string, newName string) ([]string, error) {
if strings.TrimSpace(directory) != directoryPath {
return nil, errors.New(fmt.Sprintf("我无法访问目录 %s", directory))
}

fmt.Println("Renaming file:", oldName, "to", newName)

err := os.Rename(filepath.Join(directory, oldName), filepath.Join(directory, newName))
if err != nil {
return nil, err
}
return listFiles(directory, "")
}

main.go

将两个 tools 注册并添加到服务中,最后启动了一个 SSE 的服务。注册时可以看到我们指定了输入的必要参数。具体这里就不过多解释 SSE 是什么了,当然 MCP 也提供了其他接入的方式,比如标准的输入输出等等,这里以 SSE 举例。AddTool 内的实现方法也非常简单,就是获取参数,调用前一步的 方法 ,然后处理并返回结果即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package main

import (
"context"

"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)

func main() {
s := server.NewMCPServer("File manager server", "1.0.0")

// 添加工具,查询 /tmp 下的目录
listFilesTool := mcp.NewTool("list_files", mcp.WithDescription("列出指定目录下的文件"),
mcp.WithString("directory", mcp.Required(), mcp.Description("要列出文件的目录")),
mcp.WithString("extension", mcp.Description("要过滤的文件扩展名")),
)
s.AddTool(listFilesTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
directory := request.Params.Arguments["directory"].(string)
extension := request.Params.Arguments["extension"].(string)

files, err := listFiles(directory, extension)
if err != nil {
return mcp.NewToolResultText(err.Error()), nil
}

res := "文件列表:\n"
for _, file := range files {
res += file + "\n"
}

return mcp.NewToolResultText(res), nil
})

// 添加工具,重命名文件
renameFileTool := mcp.NewTool("rename_file", mcp.WithDescription("重命名文件"),
mcp.WithString("directory", mcp.Required(), mcp.Description("要重命名文件的目录")),
mcp.WithString("old_name", mcp.Required(), mcp.Description("要重命名的旧文件名")),
mcp.WithString("new_name", mcp.Required(), mcp.Description("新的文件名")),
)
s.AddTool(renameFileTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
directory := request.Params.Arguments["directory"].(string)
oldName := request.Params.Arguments["old_name"].(string)
newName := request.Params.Arguments["new_name"].(string)

files, err := renameFile(directory, oldName, newName)
if err != nil {
return nil, err
}

res := "重命名后的文件列表:\n"
for _, file := range files {
res += file + "\n"
}

return mcp.NewToolResultText(res), nil
})

err := server.NewSSEServer(s).Start(":9999")
if err != nil {
panic(err)
}
return
}

测试

测试前请保证大模型本身支持 MCP 并正常运行,我使用的是:ollama run qwen2.5:7b

配置 MCP 服务

配置非常简单,只需要配置一个 http://127.0.0.1:9999/sse 地址就可以了

mcp-go-try-set-mcp-server-config.png

记得需要在对话前启用指定的 MCP 服务哦

mcp-go-try-set-mcp-server.png

对话测试

如果你可以看到在对话中客户端主动调用了你的 MCP 服务证明成功了

mcp-go-try-set-chat2.png

可以看到,大模型可以理解我们的要求,并调用对应所需要的 MCP 服务从而实现对应的操作。现在大模型的手已经可以伸到我们本地来咯。

扩展与总结

扩展

除了我们上面案例中提到通过 AddTool 方法告诉大模型你提供了哪一些工具,另外还有 AddResource AddPrompt 提供可访问的资源以及最佳实践的一些提示词等。

在上面的案例中我们只是简单的列表和重命名了本地的文件,你可以进一步扩展,比如制作一个本地的文件自动管理工具,自动将杂乱无序的文件以一种合理的顺序归类并整理好。

总结

就像前面提到的那样,MCP 就像是给大模型装上了 “手” ,大脑(大模型)负责处理我们说的指令,将指令拆分成各个动作,然后调用各个协调系统(MCP)最终完成这个指令的工作。相信你看完本文应该不仅能快速上手 MCP 的使用,还能体会到 MCP 的魅力所在。那么赶紧试试吧,去构建你自己的 MCP 服务吧。