什么是MCP?
Model Context Protocol (MCP) 是Anthropic推出的开放标准,旨在为大型语言模型提供统一的外部工具和数据源访问接口。在过去,如果想要让AI处理特定的数据,通常只能依赖于预训练数据或者手动上传数据,这既麻烦又低效。MCP解决了这个问题,它使得AI不再局限于静态知识库,而是能够像人类一样调用搜索引擎、访问本地文件、连接API服务等。
MCP总体架构
MCP的核心是"客户端-服务器"架构,其中MCP客户端可以连接到多个服务器。客户端是指希望通过MCP访问数据的应用程序,如CLI工具、IDE插件或AI应用。
arduino
┌─────────────┐ JSON-RPC ┌─────────────┐
│ MCP Client │◄──────────────►│ MCP Server │
│ (AI应用) │ │ (工具提供者) │
└─────────────┘ └─────────────┘
MCP协议通过JSON-RPC 2.0进行通信,核心概念包括:
- Tools: 可调用的函数,让AI执行特定操作
- Resources: 可访问的数据源,如文件、API等
- Prompts: 模板化的提示词,支持参数化
使用mcp-sdk-go构建MCP服务
要开始使用Go语言构建MCP项目,首先需要安装mcp-sdk-go库:
bash
go get github.com/voocel/mcp-sdk-go
架构设计
该SDK采用分层架构设计,清晰地分离了不同的职责:
javascript
Application Layer │ 业务逻辑:Tools, Resources, Prompts
Protocol Layer │ MCP协议:JSON-RPC, 生命周期管理
Transport Layer │ 传输实现:STDIO, SSE, Streamable HTTP
核心接口设计如下:
go
// 传输层抽象
type Transport interface {
Send(ctx context.Context, message []byte) error
Receive(ctx context.Context) ([]byte, error)
Close() error
}
// 服务器接口
type Server interface {
AddTool(name string, tool Tool, handler ToolHandler)
AddResource(uri string, resource Resource, handler ResourceHandler)
AddPrompt(name string, prompt Prompt, handler PromptHandler)
Serve(ctx context.Context, transport Transport) error
}
构建MCP服务端
以下是一个完整的MCP服务器实现示例:
go
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/voocel/mcp-sdk-go/server"
"github.com/voocel/mcp-sdk-go/protocol"
"github.com/voocel/mcp-sdk-go/transport/stdio"
)
func main() {
// 创建服务器实例
srv := server.New(server.Options{
Name: "file-manager",
Version: "1.0.0",
})
// 添加文件搜索工具
srv.AddTool("search_files", protocol.Tool{
Description: "搜索指定模式的文件",
InputSchema: protocol.ToolInputSchema{
Type: "object",
Properties: map[string]interface{}{
"pattern": map[string]interface{}{
"type": "string",
"description": "搜索模式,支持通配符",
},
"directory": map[string]interface{}{
"type": "string",
"description": "搜索目录",
"default": ".",
},
},
Required: []string{"pattern"},
},
}, handleFileSearch)
// 添加资源
srv.AddResource("file://readme", protocol.Resource{
Name: "README文件",
Description: "项目说明文档",
MimeType: "text/markdown",
}, handleReadmeResource)
// 启动服务器
transport := stdio.NewServerTransport()
if err := srv.Serve(context.Background(), transport); err != nil {
log.Fatal(err)
}
}
// 文件搜索工具处理函数
func handleFileSearch(ctx context.Context, req protocol.CallToolRequest) (*protocol.CallToolResult, error) {
pattern := req.Params.Arguments["pattern"].(string)
directory := "."
if dir, ok := req.Params.Arguments["directory"]; ok {
directory = dir.(string)
}
var results []string
err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if matched, _ := filepath.Match(pattern, info.Name()); matched {
results = append(results, path)
}
return nil
})
if err != nil {
return nil, err
}
return &protocol.CallToolResult{
Content: []protocol.Content{
protocol.TextContent{
Type: "text",
Text: strings.Join(results, "\n"),
},
},
}, nil
}
// README资源处理函数
func handleReadmeResource(ctx context.Context, req protocol.ReadResourceRequest) ([]protocol.ResourceContents, error) {
content, err := os.ReadFile("README.md")
if err != nil {
return nil, err
}
return []protocol.ResourceContents{
protocol.TextResourceContents{
URI: "file://readme",
MimeType: "text/markdown",
Text: string(content),
},
}, nil
}
构建MCP客户端
客户端的实现同样简洁:
go
package main
import (
"context"
"fmt"
"log"
"github.com/voocel/mcp-sdk-go/client"
"github.com/voocel/mcp-sdk-go/transport/stdio"
)
func main() {
// 创建客户端
mcpClient := client.New("file-client", "1.0.0")
// 使用STDIO传输连接到服务器
transport := stdio.New("./server", []string{})
if err := mcpClient.Connect(context.Background(), transport); err != nil {
log.Fatal(err)
}
defer mcpClient.Close()
// 列出可用工具
tools, err := mcpClient.ListTools(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("发现 %d 个工具:\n", len(tools.Tools))
for _, tool := range tools.Tools {
fmt.Printf("- %s: %s\n", tool.Name, tool.Description)
}
// 调用文件搜索工具
result, err := mcpClient.CallTool(context.Background(), protocol.CallToolRequest{
Params: protocol.CallToolParams{
Name: "search_files",
Arguments: map[string]interface{}{
"pattern": "*.go",
},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("搜索结果:\n%s\n", result.Content[0].(protocol.TextContent).Text)
}
三种传输方式详解
mcp-sdk-go支持三种不同的传输方式,适用于不同的使用场景。
1. STDIO传输
STDIO传输是最简单的传输方式,适用于本地进程通信:
go
type StdioTransport struct {
cmd *exec.Cmd
stdin io.WriteCloser
stdout io.ReadCloser
}
func (t *StdioTransport) Send(ctx context.Context, message []byte) error {
_, err := t.stdin.Write(append(message, '\n'))
return err
}
func (t *StdioTransport) Receive(ctx context.Context) ([]byte, error) {
scanner := bufio.NewScanner(t.stdout)
if scanner.Scan() {
return scanner.Bytes(), nil
}
return nil, scanner.Err()
}
2. SSE传输
基于Server-Sent Events的HTTP传输,适合Web应用场景:
go
type SSETransport struct {
baseURL string
httpClient *http.Client
eventChan chan []byte
}
func (t *SSETransport) Send(ctx context.Context, message []byte) error {
resp, err := t.httpClient.Post(
t.baseURL+"/message",
"application/json",
bytes.NewReader(message),
)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
3. Streamable HTTP传输
这是最新的传输协议,支持MCP 2025-03-26规范。与传统SSE需要两个端点不同,Streamable HTTP使用单一端点:
go
type StreamableTransport struct {
endpoint string
sessionID string
client *http.Client
}
func (t *StreamableTransport) Send(ctx context.Context, message []byte) error {
req, _ := http.NewRequestWithContext(ctx, "POST", t.endpoint, bytes.NewReader(message))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("MCP-Protocol-Version", "2025-03-26")
if t.sessionID != "" {
req.Header.Set("Mcp-Session-Id", t.sessionID)
}
resp, err := t.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// 提取会话ID
if sessionID := resp.Header.Get("Mcp-Session-Id"); sessionID != "" {
t.sessionID = sessionID
}
return nil
}
Streamable HTTP的技术特点
单端点设计
Streamable HTTP服务器通过HTTP方法区分不同操作:
go
func (s *StreamableServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST":
s.handleJSONRPC(w, r) // 处理JSON-RPC请求
case "GET":
s.handleEventStream(w, r) // 建立SSE流
case "DELETE":
s.handleSessionTermination(w, r) // 终止会话
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
会话管理
go
type Session struct {
ID string
Client chan []byte
LastActive time.Time
mu sync.RWMutex
}
func (s *StreamableServer) getOrCreateSession(r *http.Request) *Session {
sessionID := r.Header.Get("Mcp-Session-Id")
if sessionID == "" {
sessionID = generateSessionID()
}
s.mu.Lock()
defer s.mu.Unlock()
session, exists := s.sessions[sessionID]
if !exists {
session = &Session{
ID: sessionID,
Client: make(chan []byte, 100),
LastActive: time.Now(),
}
s.sessions[sessionID] = session
}
session.LastActive = time.Now()
return session
}
协议版本协商
go
func (s *StreamableServer) negotiateProtocol(r *http.Request) string {
clientVersion := r.Header.Get("MCP-Protocol-Version")
supportedVersions := []string{"2025-03-26", "2024-11-05"}
for _, version := range supportedVersions {
if version == clientVersion {
return version
}
}
return supportedVersions[len(supportedVersions)-1]
}
处理多态JSON序列化
MCP协议中的Content类型是多态的,需要自定义序列化处理:
go
type Content interface {
GetType() string
}
type TextContent struct {
Type string `json:"type"`
Text string `json:"text"`
}
type ImageContent struct {
Type string `json:"type"`
Data string `json:"data"`
MimeType string `json:"mimeType"`
}
func UnmarshalContent(data []byte, content *Content) error {
var base struct {
Type string `json:"type"`
}
if err := json.Unmarshal(data, &base); err != nil {
return err
}
switch base.Type {
case "text":
var textContent TextContent
if err := json.Unmarshal(data, &textContent); err != nil {
return err
}
*content = textContent
case "image":
var imageContent ImageContent
if err := json.Unmarshal(data, &imageContent); err != nil {
return err
}
*content = imageContent
default:
return fmt.Errorf("unknown content type: %s", base.Type)
}
return nil
}
性能优化考虑
并发处理
使用goroutine池避免无限制创建goroutine:
go
func (s *Server) handleRequest(ctx context.Context, req []byte) {
s.workerPool <- struct{}{}
go func() {
defer func() { <-s.workerPool }()
s.processRequest(ctx, req)
}()
}
内存管理
使用对象池减少GC压力:
go
var messagePool = sync.Pool{
New: func() interface{} {
return make([]byte, 0, 1024)
},
}
func (t *Transport) Send(ctx context.Context, message []byte) error {
buf := messagePool.Get().([]byte)
defer messagePool.Put(buf[:0])
buf = append(buf, message...)
return t.sendRaw(ctx, buf)
}
测试实践
单元测试
go
func TestStreamableTransport(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Mcp-Session-Id", "test-session")
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
transport := streamable.New(server.URL)
err := transport.Send(context.Background(), []byte(`{"test": "message"}`))
assert.NoError(t, err)
assert.Equal(t, "test-session", transport.SessionID())
}
集成测试
go
func TestMCPIntegration(t *testing.T) {
srv := server.New(server.Options{Name: "test"})
srv.AddTool("echo", echoTool, echoHandler)
transport := memory.NewTransport()
go srv.Serve(context.Background(), transport)
client := client.New("test-client", "1.0.0")
err := client.Connect(context.Background(), transport)
require.NoError(t, err)
result, err := client.CallTool(context.Background(), /* ... */)
require.NoError(t, err)
assert.NotNil(t, result)
}
传输方式对比
特性 | STDIO | SSE | Streamable HTTP |
---|---|---|---|
使用场景 | 本地进程 | Web应用 | 现代Web应用 |
端点数量 | N/A | 2个 | 1个 |
状态管理 | 无状态 | 有状态 | 可选 |
部署复杂度 | 简单 | 中等 | 简单 |
协议版本 | 全部 | 2024-11-05 | 2025-03-26 |
快速开始
- 安装依赖:
bash
go get github.com/voocel/mcp-sdk-go
- 创建基本服务器:
bash
mkdir mcp-project && cd mcp-project
go mod init mcp-project
- 运行示例:
bash
# 克隆项目
git clone https://github.com/voocel/mcp-sdk-go.git
cd mcp-sdk-go/examples/calculator
go run server/main.go
总结
通过分层架构设计,mcp-sdk-go提供了一个功能完整的MCP实现,支持三种传输方式。Streamable HTTP作为最新的传输协议,在保持简洁性的同时提供了更好的扩展性和部署灵活性。
关键技术点包括:
- 清晰的接口抽象和分层设计
- 多态JSON序列化处理
- 会话管理和协议版本协商
- 并发安全和性能优化
该SDK为Go开发者提供了构建MCP服务的完整解决方案,无论是本地工具集成还是Web服务部署,都能找到合适的传输方式。