跳到主要内容

搭建一个原生 MCP 服务

什么是 MCP 服务

MCP (Model Context Protocol) 服务是一个遵循 MCP 协议规范的服务端程序,它向 AI 客户端提供工具、资源和提示功能。通过搭建 MCP 服务,我们可以将各种外部系统(如数据库、API、文件系统等)以标准化的方式暴露给 AI Agent 使用。

环境准备

在开始之前,确保你的开发环境已经安装了以下工具:

  • Go 1.21 或更高版本
  • Git
  • 一个代码编辑器(推荐 VS Code)

首先创建项目目录并初始化 Go 模块:

mkdir mcp-server-demo
cd mcp-server-demo
go mod init github.com/yourusername/mcp-server-demo

核心依赖与项目结构

安装依赖

# 安装 JSON-RPC 库
go get github.com/sourcegraph/jsonrpc2

# 安装 UUID 生成库
go get github.com/google/uuid

# 安装日志库
go get github.com/sirupsen/logrus

# 安装配置管理库
go get github.com/spf13/viper

项目结构

mcp-server-demo/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── mcp/
│ │ ├── server.go
│ │ ├── transport.go
│ │ └── protocol.go
│ ├── tools/
│ │ ├── manager.go
│ │ ├── file_tool.go
│ │ └── calc_tool.go
│ ├── resources/
│ │ └── manager.go
│ └── config/
│ └── config.go
├── pkg/
│ └── types/
│ └── mcp.go
├── configs/
│ └── config.yaml
├── go.mod
└── README.md

定义 MCP 协议类型

首先,我们需要定义 MCP 协议的基本数据结构:

// pkg/types/mcp.go
package types

import "encoding/json"

// 基础 JSON-RPC 消息结构
type JSONRPCMessage struct {
ID interface{} `json:"id,omitempty"`
Method string `json:"method"`
Params interface{} `json:"params,omitempty"`
Result interface{} `json:"result,omitempty"`
Error *RPCError `json:"error,omitempty"`
Version string `json:"jsonrpc,omitempty"`
}

type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}

// MCP 初始化相关
type InitializeRequest struct {
ProtocolVersion string `json:"protocolVersion"`
Capabilities ClientCapabilities `json:"capabilities"`
ClientInfo ClientInfo `json:"clientInfo"`
}

type ClientCapabilities struct {
Roots RootsCapability `json:"roots,omitempty"`
Sampling SamplingCapability `json:"sampling,omitempty"`
}

type RootsCapability struct {
ListChanged bool `json:"listChanged"`
}

type SamplingCapability struct{}

type ClientInfo struct {
Name string `json:"name"`
Version string `json:"version"`
}

type InitializeResult struct {
ProtocolVersion string `json:"protocolVersion"`
Capabilities ServerCapabilities `json:"capabilities"`
ServerInfo ServerInfo `json:"serverInfo"`
}

type ServerCapabilities struct {
Tools ToolsCapability `json:"tools,omitempty"`
Resources ResourcesCapability `json:"resources,omitempty"`
Prompts PromptsCapability `json:"prompts,omitempty"`
}

type ToolsCapability struct {
ListChanged bool `json:"listChanged"`
}

type ResourcesCapability struct {
Subscribe bool `json:"subscribe"`
ListChanged bool `json:"listChanged"`
}

type PromptsCapability struct {
ListChanged bool `json:"listChanged"`
}

type ServerInfo struct {
Name string `json:"name"`
Version string `json:"version"`
}

// 工具定义
type Tool struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema ToolSchema `json:"inputSchema"`
}

type ToolSchema struct {
Type string `json:"type"`
Properties map[string]interface{} `json:"properties"`
Required []string `json:"required,omitempty"`
}

type ToolCallRequest struct {
Name string `json:"name"`
Arguments map[string]interface{} `json:"arguments,omitempty"`
}

type ToolResult struct {
Content []ToolContent `json:"content"`
IsError bool `json:"isError,omitempty"`
}

type ToolContent struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
}

// 资源定义
type Resource struct {
URI string `json:"uri"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
MimeType string `json:"mimeType,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
}

type ResourceRequest struct {
URI string `json:"uri"`
}

type ResourceResult struct {
Contents []ResourceContent `json:"contents"`
}

type ResourceContent struct {
URI string `json:"uri"`
MimeType string `json:"mimeType,omitempty"`
Text string `json:"text,omitempty"`
Blob string `json:"blob,omitempty"`
}

实现传输层

接下来实现基于 JSON-RPC 的传输层:

// internal/mcp/transport.go
package mcp

import (
"context"
"encoding/json"
"fmt"
"io"
"log"

"github.com/sourcegraph/jsonrpc2"
"github.com/yourusername/mcp-server-demo/pkg/types"
)

type Transport struct {
conn *jsonrpc2.Conn
server *Server
}

func NewTransport(rwc io.ReadWriteCloser, server *Server) *Transport {
transport := &Transport{
server: server,
}

// 创建 JSON-RPC 连接
transport.conn = jsonrpc2.NewConn(
context.Background(),
jsonrpc2.NewBufferedStream(rwc, jsonrpc2.VSCodeObjectCodec{}),
transport,
)

return transport
}

// 实现 jsonrpc2.Handler 接口
func (t *Transport) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) {
log.Printf("收到请求: %s", req.Method)

var result interface{}
var err error

switch req.Method {
case "initialize":
result, err = t.handleInitialize(req.Params)
case "initialized":
// 初始化确认,无需返回结果
return
case "tools/list":
result, err = t.handleToolsList()
case "tools/call":
result, err = t.handleToolsCall(req.Params)
case "resources/list":
result, err = t.handleResourcesList()
case "resources/read":
result, err = t.handleResourcesRead(req.Params)
default:
err = fmt.Errorf("未知方法: %s", req.Method)
}

if err != nil {
conn.ReplyWithError(ctx, req.ID, &jsonrpc2.Error{
Code: jsonrpc2.CodeMethodNotFound,
Message: err.Error(),
})
return
}

conn.Reply(ctx, req.ID, result)
}

func (t *Transport) handleInitialize(params *json.RawMessage) (*types.InitializeResult, error) {
var req types.InitializeRequest
if err := json.Unmarshal(*params, &req); err != nil {
return nil, fmt.Errorf("解析初始化请求失败: %v", err)
}

return t.server.Initialize(req)
}

func (t *Transport) handleToolsList() (map[string]interface{}, error) {
tools, err := t.server.ListTools()
if err != nil {
return nil, err
}

return map[string]interface{}{
"tools": tools,
}, nil
}

func (t *Transport) handleToolsCall(params *json.RawMessage) (*types.ToolResult, error) {
var req types.ToolCallRequest
if err := json.Unmarshal(*params, &req); err != nil {
return nil, fmt.Errorf("解析工具调用请求失败: %v", err)
}

return t.server.CallTool(req)
}

func (t *Transport) handleResourcesList() (map[string]interface{}, error) {
resources, err := t.server.ListResources()
if err != nil {
return nil, err
}

return map[string]interface{}{
"resources": resources,
}, nil
}

func (t *Transport) handleResourcesRead(params *json.RawMessage) (*types.ResourceResult, error) {
var req types.ResourceRequest
if err := json.Unmarshal(*params, &req); err != nil {
return nil, fmt.Errorf("解析资源读取请求失败: %v", err)
}

return t.server.ReadResource(req)
}

func (t *Transport) Close() error {
return t.conn.Close()
}

实现工具管理器

工具管理器负责注册和执行各种工具:

// internal/tools/manager.go
package tools

import (
"fmt"
"sync"

"github.com/yourusername/mcp-server-demo/pkg/types"
)

type Tool interface {
Name() string
Description() string
Schema() types.ToolSchema
Execute(args map[string]interface{}) (*types.ToolResult, error)
}

type Manager struct {
tools map[string]Tool
mutex sync.RWMutex
}

func NewManager() *Manager {
return &Manager{
tools: make(map[string]Tool),
}
}

func (m *Manager) RegisterTool(tool Tool) {
m.mutex.Lock()
defer m.mutex.Unlock()

m.tools[tool.Name()] = tool
}

func (m *Manager) ListTools() []types.Tool {
m.mutex.RLock()
defer m.mutex.RUnlock()

tools := make([]types.Tool, 0, len(m.tools))
for _, tool := range m.tools {
tools = append(tools, types.Tool{
Name: tool.Name(),
Description: tool.Description(),
InputSchema: tool.Schema(),
})
}

return tools
}

func (m *Manager) ExecuteTool(name string, args map[string]interface{}) (*types.ToolResult, error) {
m.mutex.RLock()
tool, exists := m.tools[name]
m.mutex.RUnlock()

if !exists {
return nil, fmt.Errorf("工具 '%s' 不存在", name)
}

return tool.Execute(args)
}

实现具体工具

让我们实现两个示例工具:文件操作工具和计算器工具。

// internal/tools/file_tool.go
package tools

import (
"fmt"
"os"
"path/filepath"

"github.com/yourusername/mcp-server-demo/pkg/types"
)

type FileTool struct{}

func NewFileTool() *FileTool {
return &FileTool{}
}

func (f *FileTool) Name() string {
return "read_file"
}

func (f *FileTool) Description() string {
return "读取指定路径的文件内容"
}

func (f *FileTool) Schema() types.ToolSchema {
return types.ToolSchema{
Type: "object",
Properties: map[string]interface{}{
"path": map[string]interface{}{
"type": "string",
"description": "要读取的文件路径",
},
},
Required: []string{"path"},
}
}

func (f *FileTool) Execute(args map[string]interface{}) (*types.ToolResult, error) {
pathInterface, exists := args["path"]
if !exists {
return &types.ToolResult{
Content: []types.ToolContent{{
Type: "text",
Text: "错误: 缺少 path 参数",
}},
IsError: true,
}, nil
}

path, ok := pathInterface.(string)
if !ok {
return &types.ToolResult{
Content: []types.ToolContent{{
Type: "text",
Text: "错误: path 参数必须是字符串",
}},
IsError: true,
}, nil
}

// 安全检查:只允许读取当前目录及子目录下的文件
absPath, err := filepath.Abs(path)
if err != nil {
return &types.ToolResult{
Content: []types.ToolContent{{
Type: "text",
Text: fmt.Sprintf("错误: 无法解析路径 %s", err.Error()),
}},
IsError: true,
}, nil
}

workDir, _ := os.Getwd()
if !filepath.HasPrefix(absPath, workDir) {
return &types.ToolResult{
Content: []types.ToolContent{{
Type: "text",
Text: "错误: 不允许访问工作目录外的文件",
}},
IsError: true,
}, nil
}

content, err := os.ReadFile(absPath)
if err != nil {
return &types.ToolResult{
Content: []types.ToolContent{{
Type: "text",
Text: fmt.Sprintf("错误: 读取文件失败 %s", err.Error()),
}},
IsError: true,
}, nil
}

return &types.ToolResult{
Content: []types.ToolContent{{
Type: "text",
Text: string(content),
}},
}, nil
}
// internal/tools/calc_tool.go
package tools

import (
"fmt"
"strconv"

"github.com/yourusername/mcp-server-demo/pkg/types"
)

type CalcTool struct{}

func NewCalcTool() *CalcTool {
return &CalcTool{}
}

func (c *CalcTool) Name() string {
return "calculator"
}

func (c *CalcTool) Description() string {
return "执行基本数学运算"
}

func (c *CalcTool) Schema() types.ToolSchema {
return types.ToolSchema{
Type: "object",
Properties: map[string]interface{}{
"operation": map[string]interface{}{
"type": "string",
"description": "运算符 (+, -, *, /)",
"enum": []string{"+", "-", "*", "/"},
},
"a": map[string]interface{}{
"type": "number",
"description": "第一个操作数",
},
"b": map[string]interface{}{
"type": "number",
"description": "第二个操作数",
},
},
Required: []string{"operation", "a", "b"},
}
}

func (c *CalcTool) Execute(args map[string]interface{}) (*types.ToolResult, error) {
// 提取参数
operation, ok := args["operation"].(string)
if !ok {
return c.errorResult("operation 参数必须是字符串"), nil
}

aFloat, err := c.toFloat64(args["a"])
if err != nil {
return c.errorResult(fmt.Sprintf("参数 a 无效: %v", err)), nil
}

bFloat, err := c.toFloat64(args["b"])
if err != nil {
return c.errorResult(fmt.Sprintf("参数 b 无效: %v", err)), nil
}

// 执行运算
var result float64
switch operation {
case "+":
result = aFloat + bFloat
case "-":
result = aFloat - bFloat
case "*":
result = aFloat * bFloat
case "/":
if bFloat == 0 {
return c.errorResult("除数不能为零"), nil
}
result = aFloat / bFloat
default:
return c.errorResult(fmt.Sprintf("不支持的运算符: %s", operation)), nil
}

return &types.ToolResult{
Content: []types.ToolContent{{
Type: "text",
Text: fmt.Sprintf("%.6g", result),
}},
}, nil
}

func (c *CalcTool) toFloat64(value interface{}) (float64, error) {
switch v := value.(type) {
case float64:
return v, nil
case float32:
return float64(v), nil
case int:
return float64(v), nil
case int64:
return float64(v), nil
case string:
return strconv.ParseFloat(v, 64)
default:
return 0, fmt.Errorf("无法转换为数字: %T", value)
}
}

func (c *CalcTool) errorResult(message string) *types.ToolResult {
return &types.ToolResult{
Content: []types.ToolContent{{
Type: "text",
Text: message,
}},
IsError: true,
}
}

实现资源管理器

资源管理器负责提供对文件和数据的访问:

// internal/resources/manager.go
package resources

import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"sync"

"github.com/yourusername/mcp-server-demo/pkg/types"
)

type Manager struct {
baseDir string
mutex sync.RWMutex
}

func NewManager(baseDir string) *Manager {
return &Manager{
baseDir: baseDir,
}
}

func (m *Manager) ListResources() ([]types.Resource, error) {
m.mutex.RLock()
defer m.mutex.RUnlock()

var resources []types.Resource

err := filepath.WalkDir(m.baseDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}

if d.IsDir() {
return nil
}

// 获取相对路径
relPath, err := filepath.Rel(m.baseDir, path)
if err != nil {
return err
}

// 跳过隐藏文件
if strings.HasPrefix(filepath.Base(relPath), ".") {
return nil
}

// 确定 MIME 类型
mimeType := m.getMimeType(filepath.Ext(path))

resource := types.Resource{
URI: fmt.Sprintf("file://%s", relPath),
Name: filepath.Base(path),
Description: fmt.Sprintf("文件: %s", relPath),
MimeType: mimeType,
}

resources = append(resources, resource)
return nil
})

if err != nil {
return nil, fmt.Errorf("扫描资源目录失败: %v", err)
}

return resources, nil
}

func (m *Manager) ReadResource(uri string) (*types.ResourceResult, error) {
m.mutex.RLock()
defer m.mutex.RUnlock()

// 解析 URI
if !strings.HasPrefix(uri, "file://") {
return nil, fmt.Errorf("不支持的 URI 方案")
}

relativePath := strings.TrimPrefix(uri, "file://")
fullPath := filepath.Join(m.baseDir, relativePath)

// 安全检查
absPath, err := filepath.Abs(fullPath)
if err != nil {
return nil, fmt.Errorf("无法解析路径: %v", err)
}

absBaseDir, err := filepath.Abs(m.baseDir)
if err != nil {
return nil, fmt.Errorf("无法解析基础目录: %v", err)
}

if !strings.HasPrefix(absPath, absBaseDir) {
return nil, fmt.Errorf("访问被拒绝: 路径在基础目录外")
}

// 读取文件内容
content, err := os.ReadFile(absPath)
if err != nil {
return nil, fmt.Errorf("读取文件失败: %v", err)
}

mimeType := m.getMimeType(filepath.Ext(absPath))

resourceContent := types.ResourceContent{
URI: uri,
MimeType: mimeType,
Text: string(content),
}

return &types.ResourceResult{
Contents: []types.ResourceContent{resourceContent},
}, nil
}

func (m *Manager) getMimeType(ext string) string {
switch strings.ToLower(ext) {
case ".txt", ".md":
return "text/plain"
case ".json":
return "application/json"
case ".html", ".htm":
return "text/html"
case ".css":
return "text/css"
case ".js":
return "application/javascript"
case ".go":
return "text/x-go"
case ".py":
return "text/x-python"
case ".yaml", ".yml":
return "application/yaml"
default:
return "application/octet-stream"
}
}

实现 MCP 服务器核心

现在让我们把所有组件整合到 MCP 服务器中:

// internal/mcp/server.go
package mcp

import (
"fmt"

"github.com/yourusername/mcp-server-demo/internal/resources"
"github.com/yourusername/mcp-server-demo/internal/tools"
"github.com/yourusername/mcp-server-demo/pkg/types"
)

type Server struct {
name string
version string
toolManager *tools.Manager
resourceManager *resources.Manager
initialized bool
}

func NewServer(name, version string) *Server {
return &Server{
name: name,
version: version,
toolManager: tools.NewManager(),
resourceManager: resources.NewManager("./data"),
initialized: false,
}
}

func (s *Server) Initialize(req types.InitializeRequest) (*types.InitializeResult, error) {
if s.initialized {
return nil, fmt.Errorf("服务器已经初始化")
}

// 注册工具
s.toolManager.RegisterTool(tools.NewFileTool())
s.toolManager.RegisterTool(tools.NewCalcTool())

s.initialized = true

return &types.InitializeResult{
ProtocolVersion: "2024-11-05",
Capabilities: types.ServerCapabilities{
Tools: types.ToolsCapability{
ListChanged: false,
},
Resources: types.ResourcesCapability{
Subscribe: false,
ListChanged: false,
},
},
ServerInfo: types.ServerInfo{
Name: s.name,
Version: s.version,
},
}, nil
}

func (s *Server) ListTools() ([]types.Tool, error) {
if !s.initialized {
return nil, fmt.Errorf("服务器未初始化")
}

return s.toolManager.ListTools(), nil
}

func (s *Server) CallTool(req types.ToolCallRequest) (*types.ToolResult, error) {
if !s.initialized {
return nil, fmt.Errorf("服务器未初始化")
}

return s.toolManager.ExecuteTool(req.Name, req.Arguments)
}

func (s *Server) ListResources() ([]types.Resource, error) {
if !s.initialized {
return nil, fmt.Errorf("服务器未初始化")
}

return s.resourceManager.ListResources()
}

func (s *Server) ReadResource(req types.ResourceRequest) (*types.ResourceResult, error) {
if !s.initialized {
return nil, fmt.Errorf("服务器未初始化")
}

return s.resourceManager.ReadResource(req.URI)
}

实现主程序

最后,创建服务器的主程序入口:

// cmd/server/main.go
package main

import (
"flag"
"log"
"os"

"github.com/yourusername/mcp-server-demo/internal/mcp"
)

func main() {
var (
name = flag.String("name", "MCP Demo Server", "服务器名称")
version = flag.String("version", "1.0.0", "服务器版本")
)
flag.Parse()

// 创建 MCP 服务器
server := mcp.NewServer(*name, *version)

// 创建传输层(使用标准输入输出)
transport := mcp.NewTransport(struct {
*os.File
*os.File
}{os.Stdin, os.Stdout}, server)

log.Printf("MCP 服务器启动: %s v%s", *name, *version)

// 等待连接关闭
<-transport.conn.DisconnectNotify()

log.Println("MCP 服务器关闭")
}

创建配置和数据文件

创建一个示例数据目录和一些测试文件:

mkdir -p data
echo "Hello, MCP World!" > data/hello.txt
echo '{"name": "test", "value": 42}' > data/config.json

创建配置文件:

# configs/config.yaml
server:
name: "MCP Demo Server"
version: "1.0.0"
data_dir: "./data"

tools:
enabled:
- "read_file"
- "calculator"

resources:
base_dir: "./data"
allowed_extensions:
- ".txt"
- ".json"
- ".md"
- ".yaml"

构建和测试

构建服务器

go build -o bin/mcp-server cmd/server/main.go

创建测试客户端

为了测试我们的 MCP 服务器,让我们创建一个简单的测试客户端:

// cmd/client/main.go
package main

import (
"context"
"encoding/json"
"fmt"
"log"
"os/exec"

"github.com/sourcegraph/jsonrpc2"
"github.com/yourusername/mcp-server-demo/pkg/types"
)

func main() {
// 启动 MCP 服务器作为子进程
cmd := exec.Command("./bin/mcp-server")
stdin, err := cmd.StdinPipe()
if err != nil {
log.Fatal(err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
}

if err := cmd.Start(); err != nil {
log.Fatal(err)
}
defer cmd.Process.Kill()

// 创建 JSON-RPC 客户端
conn := jsonrpc2.NewConn(
context.Background(),
jsonrpc2.NewBufferedStream(struct {
*os.File
*os.File
}{stdout, stdin}, jsonrpc2.VSCodeObjectCodec{}),
jsonrpc2.HandlerWithError(func(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (interface{}, error) {
log.Printf("服务器发送通知: %s", req.Method)
return nil, nil
}),
)
defer conn.Close()

// 1. 初始化连接
var initResult types.InitializeResult
err = conn.Call(context.Background(), "initialize", types.InitializeRequest{
ProtocolVersion: "2024-11-05",
Capabilities: types.ClientCapabilities{
Roots: types.RootsCapability{ListChanged: true},
},
ClientInfo: types.ClientInfo{
Name: "Test Client",
Version: "1.0.0",
},
}, &initResult)

if err != nil {
log.Fatalf("初始化失败: %v", err)
}

fmt.Printf("服务器初始化成功: %s v%s\n",
initResult.ServerInfo.Name, initResult.ServerInfo.Version)

// 发送 initialized 通知
err = conn.Notify(context.Background(), "initialized", nil)
if err != nil {
log.Fatalf("发送 initialized 通知失败: %v", err)
}

// 2. 列出可用工具
var toolsResult struct {
Tools []types.Tool `json:"tools"`
}
err = conn.Call(context.Background(), "tools/list", nil, &toolsResult)
if err != nil {
log.Fatalf("获取工具列表失败: %v", err)
}

fmt.Println("\n可用工具:")
for _, tool := range toolsResult.Tools {
fmt.Printf("- %s: %s\n", tool.Name, tool.Description)
}

// 3. 测试计算器工具
var calcResult types.ToolResult
err = conn.Call(context.Background(), "tools/call", types.ToolCallRequest{
Name: "calculator",
Arguments: map[string]interface{}{
"operation": "+",
"a": 10,
"b": 5,
},
}, &calcResult)

if err != nil {
log.Fatalf("调用计算器工具失败: %v", err)
}

fmt.Printf("\n计算结果: %s\n", calcResult.Content[0].Text)

// 4. 列出资源
var resourcesResult struct {
Resources []types.Resource `json:"resources"`
}
err = conn.Call(context.Background(), "resources/list", nil, &resourcesResult)
if err != nil {
log.Fatalf("获取资源列表失败: %v", err)
}

fmt.Println("\n可用资源:")
for _, resource := range resourcesResult.Resources {
fmt.Printf("- %s (%s)\n", resource.Name, resource.URI)
}

// 5. 读取文件资源
if len(resourcesResult.Resources) > 0 {
var readResult types.ResourceResult
err = conn.Call(context.Background(), "resources/read", types.ResourceRequest{
URI: resourcesResult.Resources[0].URI,
}, &readResult)

if err != nil {
log.Fatalf("读取资源失败: %v", err)
}

fmt.Printf("\n文件内容:\n%s\n", readResult.Contents[0].Text)
}
}

构建并运行测试:

go build -o bin/test-client cmd/client/main.go
./bin/test-client

性能优化与监控

添加日志和监控

// internal/mcp/middleware.go
package mcp

import (
"context"
"log"
"time"

"github.com/sourcegraph/jsonrpc2"
)

type LoggingHandler struct {
next jsonrpc2.Handler
}

func NewLoggingHandler(next jsonrpc2.Handler) *LoggingHandler {
return &LoggingHandler{next: next}
}

func (h *LoggingHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) {
start := time.Now()

log.Printf("开始处理请求: %s (ID: %v)", req.Method, req.ID)

h.next.Handle(ctx, conn, req)

duration := time.Since(start)
log.Printf("完成请求处理: %s, 耗时: %v", req.Method, duration)
}

添加错误处理

// internal/mcp/errors.go
package mcp

import "fmt"

type MCPError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}

func (e *MCPError) Error() string {
return fmt.Sprintf("MCP错误 %d: %s", e.Code, e.Message)
}

const (
ErrorCodeInvalidRequest = -32600
ErrorCodeMethodNotFound = -32601
ErrorCodeInvalidParams = -32602
ErrorCodeInternalError = -32603
ErrorCodeToolNotFound = -1001
ErrorCodeResourceNotFound = -1002
)

func NewToolNotFoundError(toolName string) *MCPError {
return &MCPError{
Code: ErrorCodeToolNotFound,
Message: fmt.Sprintf("工具 '%s' 未找到", toolName),
}
}

func NewResourceNotFoundError(uri string) *MCPError {
return &MCPError{
Code: ErrorCodeResourceNotFound,
Message: fmt.Sprintf("资源 '%s' 未找到", uri),
}
}

部署和使用

Docker 部署

创建 Dockerfile:

# Dockerfile
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN go build -o bin/mcp-server cmd/server/main.go

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/

COPY --from=builder /app/bin/mcp-server .
COPY --from=builder /app/data ./data

CMD ["./mcp-server"]

构建 Docker 镜像:

docker build -t mcp-server:latest .

在 Claude Desktop 中配置

如果要在 Claude Desktop 中使用,需要在配置文件中添加:

{
"mcpServers": {
"demo-server": {
"command": "path/to/your/mcp-server",
"args": ["--name", "Demo Server"]
}
}
}

总结

通过本教程,我们成功搭建了一个功能完整的 MCP 服务器,它包含了:

  1. 标准化协议实现: 完全符合 MCP 规范的 JSON-RPC 通信
  2. 工具系统: 可扩展的工具注册和执行框架
  3. 资源管理: 安全的文件系统访问和资源提供
  4. 错误处理: 完善的错误处理和用户反馈机制
  5. 性能监控: 日志记录和性能追踪

这个 MCP 服务器可以作为基础框架,你可以根据需要添加更多工具和资源类型,比如:

  • 数据库连接工具
  • API 调用工具
  • 图像处理工具
  • 机器学习模型调用
  • 云服务集成

通过 MCP 协议,AI Agent 可以轻松地与你的服务器交互,获得强大的外部能力扩展。