如何使用 Go (Golang) 编写一个 Model Context Protocol (MCP) 服务器。
MCP 是一种允许工具(如 Claude AI)与外部数据源、应用程序或服务安全交互的协议。编写一个 MCP Server 就是创建一个遵循该协议的进程,通过 STDIN/STDOUT 与客户端进行 JSON-RPC 通信。
核心概念
一个 MCP 服务器需要实现以下核心组件:
-
传输层 (Transport): 通过 STDIN/STDOUT 进行 JSON-RPC 2.0 通信。
-
资源 (Resources): 提供可读取的数据流(如文件、数据库内容、API 数据)。
-
工具 (Tools): 提供可执行的操作(如执行命令、写入数据、调用外部API)。
-
提示词模板 (Prompts): 提供预定义的提示词片段。
实现步骤
我们将使用官方提供的 Go SDK github.com/modelcontextprotocol/sdk-go 来简化开发。
第 1 步:初始化项目并安装依赖
mkdir my-mcp-server
cd my-mcp-server
go mod init my-mcp-server
go get github.com/modelcontextprotocol/sdk-go
第 2 步:创建主文件 main.go
以下是创建一个简单 MCP 服务器的完整示例,它包含资源、工具和提示词模板。
package main
import (
"context"
"fmt"
"log"
"strings"
mcp "github.com/modelcontextprotocol/sdk-go"
)
func main() {
// 1. 创建服务器实例
server := mcp.NewServer(
"my-file-server", // 服务器名称
"1.0.0", // 版本号
)
// 2. 添加资源:提供一个虚拟的 "notes" 目录
server.AddResourceHandler("notes", func(ctx context.Context, method string, params map[string]any) (mcp.ResourceResult, error) {
// method 可以是 "read" 或 "list"
name, _ := params["name"].(string)
if method == "list" {
// 返回可用笔记列表
return mcp.ResourceResult{
Contents: []mcp.ResourceContent{
{
URI: "notes:///todo.txt",
// 在 list 操作中,content 是可选的,通常用于预览
},
{
URI: "notes:///ideas.txt",
},
},
}, nil
}
// method == "read"
// 根据请求的 URI 返回不同的内容
switch name {
case "notes:///todo.txt":
return mcp.ResourceResult{
Contents: []mcp.ResourceContent{
{
URI: "notes:///todo.txt",
Content: "1. Learn MCP\n2. Build a server\n3. Integrate with Claude",
MimeType: "text/plain",
},
},
}, nil
case "notes:///ideas.txt":
return mcp.ResourceResult{
Contents: []mcp.ResourceContent{
{
URI: "notes:///ideas.txt",
Content: "Project Idea: AI-powered task manager\nAnother Idea: Automated code reviewer",
MimeType: "text/plain",
},
},
}, nil
default:
return mcp.ResourceResult{}, fmt.Errorf("note not found: %s", name)
}
})
// 3. 添加工具:一个简单的字符串处理工具
server.AddToolHandler("reverse", func(ctx context.Context, params map[string]any) (mcp.ToolResult, error) {
input, ok := params["input"].(string)
if !ok {
return mcp.ToolResult{}, fmt.Errorf("input parameter is required and must be a string")
}
// 反转字符串
runes := []rune(input)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
result := string(runes)
return mcp.ToolResult{
Content: []mcp.Content{
{
Type: "text",
Text: fmt.Sprintf("Original: %s\nReversed: %s", input, result),
},
},
}, nil
}, mcp.ParamSchema{
Type: mcp.JSONSchemaObject,
Properties: map[string]mcp.ParamSchema{
"input": {
Type: mcp.JSONSchemaString,
Description: "The string to reverse",
},
},
Required: []string{"input"},
})
// 4. 添加提示词模板
server.AddPromptHandler("greeting", func(ctx context.Context, params map[string]any) (mcp.PromptResult, error) {
name, _ := params["name"].(string)
if name == "" {
name = "there"
}
return mcp.PromptResult{
Messages: []mcp.PromptMessage{
{
Role: "user",
Content: []mcp.Content{
{
Type: "text",
Text: fmt.Sprintf("Hello %s! I'm your MCP server assistant. How can I help you today?", name),
},
},
},
},
}, nil
}, mcp.ParamSchema{
Type: mcp.JSONSchemaObject,
Properties: map[string]mcp.ParamSchema{
"name": {
Type: mcp.JSONSchemaString,
Description: "Name of the person to greet",
},
},
})
// 5. 启动服务器
if err := server.Run(); err != nil {
log.Fatalf("Server error: %v", err)
}
}
第 3 步:构建和测试
# 构建
go build -o my-mcp-server
# 测试 STDIN/STDOUT 通信(可选,手动测试)
echo '{"jsonrpc":"2.0","id":1,"method":"listResources"}' | ./my-mcp-server
第 4 步:配置客户端(以 Claude AI 为例)
创建配置文件 ~/.config/claude/claude_desktop_config.json:
{
"mcpServers": {
"my-file-server": {
"command": "/absolute/path/to/your/my-mcp-server"
}
}
}
或者在项目根目录创建 mcp.json:
{
"mcpServers": {
"my-file-server": {
"command": "go",
"args": ["run", "/absolute/path/to/your/main.go"]
}
}
}
更高级的示例:文件系统资源
以下是一个更实用的示例,提供对真实文件系统的访问:
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
mcp "github.com/modelcontextprotocol/sdk-go"
)
type FileServer struct {
baseDir string
}
func NewFileServer(baseDir string) *FileServer {
return &FileServer{baseDir: baseDir}
}
func (s *FileServer) HandleResource(ctx context.Context, method string, params map[string]any) (mcp.ResourceResult, error) {
uri, ok := params["name"].(string)
if !ok {
return mcp.ResourceResult{}, fmt.Errorf("name parameter is required")
}
// 将 URI 转换为本地文件路径
localPath := filepath.Join(s.baseDir, strings.TrimPrefix(uri, "file:///"))
if method == "list" {
return s.listFiles(localPath)
}
// method == "read"
return s.readFile(localPath, uri)
}
func (s *FileServer) listFiles(dirPath string) (mcp.ResourceResult, error) {
entries, err := os.ReadDir(dirPath)
if err != nil {
return mcp.ResourceResult{}, err
}
contents := []mcp.ResourceContent{}
for _, entry := range entries {
uri := "file:///" + entry.Name()
if entry.IsDir() {
uri += "/"
}
contents = append(contents, mcp.ResourceContent{
URI: uri,
Name: entry.Name(),
})
}
return mcp.ResourceResult{Contents: contents}, nil
}
func (s *FileServer) readFile(filePath, uri string) (mcp.ResourceResult, error) {
content, err := os.ReadFile(filePath)
if err != nil {
return mcp.ResourceResult{}, err
}
return mcp.ResourceResult{
Contents: []mcp.ResourceContent{
{
URI: uri,
Content: string(content),
MimeType: "text/plain", // 可以根据文件扩展名设置正确的 MIME 类型
},
},
}, nil
}
func main() {
server := mcp.NewServer("file-server", "1.0.0")
fileServer := NewFileServer("/tmp") // 提供 /tmp 目录的访问
server.AddResourceHandler("files", fileServer.HandleResource)
if err := server.Run(); err != nil {
log.Fatalf("Server error: %v", err)
}
}
关键要点
-
JSON-RPC 协议: 所有通信都通过 STDIN/STDOUT 使用 JSON-RPC 2.0。
-
错误处理: 始终返回适当的错误信息。
-
模式定义: 为工具和提示词定义清晰的参数模式。
-
资源URI: 使用合适的 URI 方案(如
file:///,notes:///)。 -
内容类型: 正确设置 MIME 类型以便客户端正确处理内容。
调试技巧
-
日志记录: 使用
log.Printf()输出调试信息(客户端通常会捕获 STDERR)。 -
手动测试: 通过 echo 命令手动发送 JSON-RPC 请求来测试服务器。
-
客户端日志: 查看 Claude 或其他客户端的日志来了解通信详情。
这个基础框架应该能帮助你开始构建自己的 MCP 服务器。根据你的具体需求,你可以实现更复杂的资源、工具和提示词模板。
非特殊说明,本博所有文章均为博主原创。