Week 3 Day 20:MCP Server - 苏格拉底教学

你用 3 周时间构建了工具、RAG、审批流、权限系统。现在问题来了:这些能力只能给你自己的 Agent 用。如果同事想用 Claude Desktop、Cursor 或另一个 Agent 来调用你的工具,他们怎么接入?今天我们用 MCP 把答案做出来。


第一部分:问题驱动

问题1:为什么需要 MCP?

场景还原:

你已经有:

  • search_kb - 搜索知识库
  • create_ticket - 创建工单
  • get_user_info - 查询用户信息

同事 A 用 Claude Desktop,同事 B 用 Cursor AI,同事 C 在做自己的 Python Agent。他们都想用你的 tools。

没有 MCP 时,你要做什么?

Claude Desktop → 你需要写 Claude Desktop 插件适配层 Cursor AI → 你需要写 Cursor 格式的适配层 Python Agent → 你需要写 REST API + 文档 + SDK ... 每个平台各不同

有 MCP 后:

你写一次 MCP Server ↓ Claude Desktop / Cursor / 任何 MCP-compatible client 直接接入

引导问题:

  1. MCP 类似哪个领域的 "标准化" 先例?(USB 标准、LSP 协议、OpenAPI)
  2. MCP 是 Anthropic 的私有协议吗,还是开放标准?
  3. MCP Server 运行在哪里?是和 Agent 同一个进程吗?

问题2:MCP 的核心架构是什么?

MCP = Model Context Protocol

Anthropic 在 2024 年发布,定义了 LLM 和外部工具/数据源之间的标准接口。

三个核心概念:

MCP Client (Claude Desktop / Agent) ↕ JSON-RPC 2.0 MCP Server (你今天要写的) ↕ 你的业务逻辑 实际的工具/数据库/API

MCP Server 提供的能力:

能力 说明 例子
Tools LLM 可以调用的函数 search_kb, create_ticket
Resources LLM 可以读取的数据 文件、数据库记录
Prompts 预定义的 prompt 模板 系统 prompt、few-shot 示例

今天我们主要实现 Tools


问题3:两种 Transport 的区别?

Transport = MCP Client 和 Server 如何通信

stdio transport(本地进程):

Claude Desktop ↓ 启动子进程 MCP Server (你的 Go 程序) ↕ stdin/stdout 传 JSON-RPC 消息
  • 适合:本地工具、开发调试
  • 优点:简单、安全(不暴露网络端口)
  • 缺点:只能本地用,不能多用户共享

SSE transport(HTTP 网络):

多个 Agent / Claude Desktop ↓ HTTP 连接 MCP Server (你的 Go 程序,跑在服务器上) ↕ Server-Sent Events 传推送消息 HTTP POST 传请求消息
  • 适合:团队共享、生产部署
  • 优点:一个 Server 服务多个 Client
  • 缺点:需要处理认证、网络安全

今天先实现 stdio(更容易验证),理解 SSE 的设计。


第二部分:动手实现

版本1:理解 JSON-RPC 2.0 消息格式

MCP 基于 JSON-RPC 2.0。先理解消息格式:

// internal/mcp/protocol.go
package mcp

// JSON-RPC 2.0 基础结构
type JSONRPCRequest struct {
    JSONRPC string      `json:"jsonrpc"` // 固定 "2.0"
    ID      interface{} `json:"id"`      // 请求 ID(int 或 string)
    Method  string      `json:"method"`  // 方法名
    Params  interface{} `json:"params,omitempty"`
}

type JSONRPCResponse struct {
    JSONRPC string      `json:"jsonrpc"`
    ID      interface{} `json:"id"`
    Result  interface{} `json:"result,omitempty"`
    Error   *RPCError   `json:"error,omitempty"`
}

type RPCError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

// MCP 标准方法
const (
    MethodInitialize     = "initialize"
    MethodToolsList      = "tools/list"
    MethodToolsCall      = "tools/call"
    MethodResourcesList  = "resources/list"
    MethodPromptsList    = "prompts/list"
    MethodPing           = "ping"
)

// initialize 请求的参数
type InitializeParams struct {
    ProtocolVersion string         `json:"protocolVersion"`
    Capabilities    ClientCapabilities `json:"capabilities"`
    ClientInfo      ClientInfo     `json:"clientInfo"`
}

type ClientCapabilities struct {
    Tools     *struct{} `json:"tools,omitempty"`
    Resources *struct{} `json:"resources,omitempty"`
}

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

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

type ServerCapabilities struct {
    Tools     *ToolsCapability `json:"tools,omitempty"`
}

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

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

// tools/list 响应
type Tool struct {
    Name        string      `json:"name"`
    Description string      `json:"description"`
    InputSchema InputSchema `json:"inputSchema"`
}

type InputSchema struct {
    Type       string              `json:"type"` // "object"
    Properties map[string]Property `json:"properties"`
    Required   []string            `json:"required,omitempty"`
}

type Property struct {
    Type        string `json:"type"`
    Description string `json:"description"`
}

// tools/call 请求参数
type ToolCallParams struct {
    Name      string                 `json:"name"`
    Arguments map[string]interface{} `json:"arguments"`
}

// tools/call 响应
type ToolCallResult struct {
    Content []Content `json:"content"`
    IsError bool      `json:"isError,omitempty"`
}

type Content struct {
    Type string `json:"type"` // "text", "image", "resource"
    Text string `json:"text,omitempty"`
}

版本2:MCP Server 核心框架

// internal/mcp/server.go
package mcp

import (
    "bufio"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "log/slog"
    "os"
)

// ToolHandler 是工具的执行函数签名
type ToolHandler func(ctx context.Context, args map[string]interface{}) (*ToolCallResult, error)

// RegisteredTool 注册到 MCP Server 的工具
type RegisteredTool struct {
    Definition Tool
    Handler    ToolHandler
}

// Server MCP Server 主体
type Server struct {
    name     string
    version  string
    tools    map[string]*RegisteredTool
    reader   *bufio.Reader
    writer   io.Writer
}

func NewServer(name, version string) *Server {
    return &Server{
        name:    name,
        version: version,
        tools:   make(map[string]*RegisteredTool),
        reader:  bufio.NewReader(os.Stdin),
        writer:  os.Stdout,
    }
}

// RegisterTool 注册一个工具
func (s *Server) RegisterTool(tool Tool, handler ToolHandler) {
    s.tools[tool.Name] = &RegisteredTool{
        Definition: tool,
        Handler:    handler,
    }
    slog.Info("tool registered", "name", tool.Name)
}

// Run 启动 stdio 传输的主循环
func (s *Server) Run(ctx context.Context) error {
    slog.Info("MCP server started", "name", s.name, "version", s.version)

    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
        }

        // 从 stdin 读取一行(每条消息一行 JSON)
        line, err := s.reader.ReadString('\n')
        if err != nil {
            if err == io.EOF {
                return nil // Client 关闭了连接
            }
            return fmt.Errorf("read error: %w", err)
        }

        // 解析并处理请求
        if err := s.handleMessage(ctx, []byte(line)); err != nil {
            slog.Error("handle message error", "error", err)
        }
    }
}

// handleMessage 解析 JSON-RPC 请求并路由
func (s *Server) handleMessage(ctx context.Context, data []byte) error {
    var req JSONRPCRequest
    if err := json.Unmarshal(data, &req); err != nil {
        return s.sendError(nil, -32700, "parse error")
    }

    slog.Debug("received request", "method", req.Method, "id", req.ID)

    switch req.Method {
    case MethodInitialize:
        return s.handleInitialize(req)
    case MethodToolsList:
        return s.handleToolsList(req)
    case MethodToolsCall:
        return s.handleToolsCall(ctx, req)
    case MethodPing:
        return s.sendResult(req.ID, map[string]interface{}{})
    default:
        return s.sendError(req.ID, -32601, "method not found: "+req.Method)
    }
}

func (s *Server) handleInitialize(req JSONRPCRequest) error {
    result := InitializeResult{
        ProtocolVersion: "2024-11-05",
        Capabilities: ServerCapabilities{
            Tools: &ToolsCapability{ListChanged: false},
        },
        ServerInfo: ServerInfo{
            Name:    s.name,
            Version: s.version,
        },
    }
    return s.sendResult(req.ID, result)
}

func (s *Server) handleToolsList(req JSONRPCRequest) error {
    tools := make([]Tool, 0, len(s.tools))
    for _, t := range s.tools {
        tools = append(tools, t.Definition)
    }
    return s.sendResult(req.ID, map[string]interface{}{"tools": tools})
}

func (s *Server) handleToolsCall(ctx context.Context, req JSONRPCRequest) error {
    // 解析参数
    paramsBytes, _ := json.Marshal(req.Params)
    var params ToolCallParams
    if err := json.Unmarshal(paramsBytes, &params); err != nil {
        return s.sendError(req.ID, -32602, "invalid params")
    }

    // 查找工具
    tool, ok := s.tools[params.Name]
    if !ok {
        return s.sendError(req.ID, -32601, fmt.Sprintf("tool %q not found", params.Name))
    }

    // 执行工具
    result, err := tool.Handler(ctx, params.Arguments)
    if err != nil {
        // 工具执行失败,但这是业务错误,用 isError=true 返回(不是 RPC 错误)
        result = &ToolCallResult{
            Content: []Content{{Type: "text", Text: "Error: " + err.Error()}},
            IsError: true,
        }
    }

    return s.sendResult(req.ID, result)
}

// sendResult 发送成功响应
func (s *Server) sendResult(id interface{}, result interface{}) error {
    resp := JSONRPCResponse{
        JSONRPC: "2.0",
        ID:      id,
        Result:  result,
    }
    return s.writeJSON(resp)
}

// sendError 发送错误响应
func (s *Server) sendError(id interface{}, code int, message string) error {
    resp := JSONRPCResponse{
        JSONRPC: "2.0",
        ID:      id,
        Error:   &RPCError{Code: code, Message: message},
    }
    return s.writeJSON(resp)
}

func (s *Server) writeJSON(v interface{}) error {
    data, err := json.Marshal(v)
    if err != nil {
        return err
    }
    // MCP stdio: 每条消息后加换行
    data = append(data, '\n')
    _, err = s.writer.Write(data)
    return err
}

版本3:暴露 Week 1 的工具

// cmd/mcp/main.go
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log/slog"
    "os"
    "your-project/internal/mcp"
)

func main() {
    // 日志写到 stderr(stdout 给 MCP 消息)
    logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
        Level: slog.LevelDebug,
    }))
    slog.SetDefault(logger)

    server := mcp.NewServer("agent-tools-server", "0.3.0")

    // 注册 search_kb
    server.RegisterTool(mcp.Tool{
        Name:        "search_kb",
        Description: "搜索内部知识库。根据用户的自然语言查询,返回最相关的知识条目。",
        InputSchema: mcp.InputSchema{
            Type: "object",
            Properties: map[string]mcp.Property{
                "query": {
                    Type:        "string",
                    Description: "搜索查询,支持自然语言",
                },
                "top_k": {
                    Type:        "integer",
                    Description: "返回结果数量,默认 5",
                },
            },
            Required: []string{"query"},
        },
    }, handleSearchKB)

    // 注册 create_ticket
    server.RegisterTool(mcp.Tool{
        Name:        "create_ticket",
        Description: "创建支持工单。工单会进入审批流程,需要 L2 Support 审批后生效。",
        InputSchema: mcp.InputSchema{
            Type: "object",
            Properties: map[string]mcp.Property{
                "title": {
                    Type:        "string",
                    Description: "工单标题,简洁描述问题",
                },
                "description": {
                    Type:        "string",
                    Description: "工单详细描述",
                },
                "priority": {
                    Type:        "string",
                    Description: "优先级:low / medium / high / critical",
                },
            },
            Required: []string{"title", "description"},
        },
    }, handleCreateTicket)

    // 注册 get_user_info
    server.RegisterTool(mcp.Tool{
        Name:        "get_user_info",
        Description: "查询员工基本信息(需要 HR 权限)。返回姓名、部门、入职时间等。",
        InputSchema: mcp.InputSchema{
            Type: "object",
            Properties: map[string]mcp.Property{
                "user_id": {
                    Type:        "string",
                    Description: "员工 ID(格式:EMP-XXXXX)",
                },
                "fields": {
                    Type:        "string",
                    Description: "需要返回的字段,逗号分隔,如 name,department,hire_date",
                },
            },
            Required: []string{"user_id"},
        },
    }, handleGetUserInfo)

    ctx := context.Background()
    if err := server.Run(ctx); err != nil {
        slog.Error("server error", "error", err)
        os.Exit(1)
    }
}

// handleSearchKB 搜索知识库的实际逻辑
func handleSearchKB(ctx context.Context, args map[string]interface{}) (*mcp.ToolCallResult, error) {
    query, _ := args["query"].(string)
    if query == "" {
        return nil, fmt.Errorf("query is required")
    }

    topK := 5
    if k, ok := args["top_k"].(float64); ok {
        topK = int(k)
    }

    // TODO: 这里接入真实的 RAG retriever
    // retriever := rag.NewSecureRetriever(...)
    // chunks, err := retriever.Search(ctx, "system_user", query, topK)

    // Mock 返回
    results := []map[string]string{
        {"chunk_id": "faq_001_chunk_0", "text": "SSO 重置流程:访问 SSO 门户,点击忘记密码...", "score": "0.92"},
        {"chunk_id": "faq_001_chunk_1", "text": "工单审批流程:草稿 → 人工审核 → 批准/拒绝", "score": "0.87"},
    }
    if len(results) > topK {
        results = results[:topK]
    }

    text, _ := json.MarshalIndent(map[string]interface{}{
        "query":   query,
        "results": results,
        "count":   len(results),
    }, "", "  ")

    return &mcp.ToolCallResult{
        Content: []mcp.Content{{Type: "text", Text: string(text)}},
    }, nil
}

func handleCreateTicket(ctx context.Context, args map[string]interface{}) (*mcp.ToolCallResult, error) {
    title, _ := args["title"].(string)
    description, _ := args["description"].(string)
    priority, _ := args["priority"].(string)

    if title == "" || description == "" {
        return nil, fmt.Errorf("title and description are required")
    }
    if priority == "" {
        priority = "medium"
    }

    // TODO: 接入真实的工单系统和审批流
    ticket := map[string]string{
        "ticket_id":   "TKT-2024-001",
        "title":       title,
        "description": description,
        "priority":    priority,
        "status":      "pending_approval",
        "message":     "工单已创建,等待 L2 Support 审批",
    }

    text, _ := json.MarshalIndent(ticket, "", "  ")
    return &mcp.ToolCallResult{
        Content: []mcp.Content{{Type: "text", Text: string(text)}},
    }, nil
}

func handleGetUserInfo(ctx context.Context, args map[string]interface{}) (*mcp.ToolCallResult, error) {
    userID, _ := args["user_id"].(string)
    if userID == "" {
        return nil, fmt.Errorf("user_id is required")
    }

    // TODO: 接入真实的 HR 系统,并检查调用方权限(通过 ctx 中的 user_id)
    userInfo := map[string]string{
        "user_id":    userID,
        "name":       "张三",
        "department": "Engineering",
        "hire_date":  "2022-03-15",
        "manager":    "李四",
    }

    text, _ := json.MarshalIndent(userInfo, "", "  ")
    return &mcp.ToolCallResult{
        Content: []mcp.Content{{Type: "text", Text: string(text)}},
    }, nil
}

版本4:构建并本地测试

# 构建 MCP Server
go build -o bin/mcp-server ./cmd/mcp

# 手动测试:模拟 MCP Client 发送消息

# 1. initialize
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | ./bin/mcp-server

# 2. 获取工具列表
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./bin/mcp-server

# 3. 调用 search_kb
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_kb","arguments":{"query":"如何重置密码","top_k":3}}}' | ./bin/mcp-server

预期输出:

{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{"listChanged":false}},"serverInfo":{"name":"agent-tools-server","version":"0.3.0"}}}

版本5:配置 Claude Desktop 接入

创建配置文件 ~/.config/claude/claude_desktop_config.json(Mac 路径):

{
  "mcpServers": {
    "agent-tools": {
      "command": "/absolute/path/to/bin/mcp-server",
      "args": [],
      "env": {
        "LOG_LEVEL": "info"
      }
    }
  }
}

验证步骤:

  1. 重启 Claude Desktop
  2. 打开新对话,应该能看到锤子图标(工具)
  3. 问 Claude:"搜索知识库,查询 SSO 重置方法"
  4. Claude 会自动调用 search_kb 工具

版本6:SSE Transport(了解原理)

// internal/mcp/sse_server.go
package mcp

import (
    "encoding/json"
    "fmt"
    "log/slog"
    "net/http"
    "sync"
)

// SSEServer 基于 HTTP + SSE 的 MCP Server
// Client → POST /message   发送请求
// Client ← GET  /sse       接收推送(Server-Sent Events)
type SSEServer struct {
    *Server
    clients sync.Map // sessionID → chan string
}

func NewSSEServer(name, version string) *SSEServer {
    return &SSEServer{
        Server: NewServer(name, version),
    }
}

func (s *SSEServer) SetupRoutes(mux *http.ServeMux) {
    // Client 建立 SSE 连接
    mux.HandleFunc("/sse", s.handleSSE)
    // Client 发送 JSON-RPC 请求
    mux.HandleFunc("/message", s.handleMessage)
}

func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) {
    sessionID := r.URL.Query().Get("session_id")
    if sessionID == "" {
        http.Error(w, "session_id required", http.StatusBadRequest)
        return
    }

    // 设置 SSE 头
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")
    w.Header().Set("Access-Control-Allow-Origin", "*")

    // 创建这个 session 的消息通道
    msgChan := make(chan string, 100)
    s.clients.Store(sessionID, msgChan)
    defer s.clients.Delete(sessionID)

    slog.Info("SSE client connected", "session_id", sessionID)

    // 发送 endpoint 事件(告诉 client 在哪里发请求)
    fmt.Fprintf(w, "event: endpoint\ndata: /message?session_id=%s\n\n", sessionID)
    w.(http.Flusher).Flush()

    // 监听消息并推送
    for {
        select {
        case msg := <-msgChan:
            fmt.Fprintf(w, "data: %s\n\n", msg)
            w.(http.Flusher).Flush()
        case <-r.Context().Done():
            slog.Info("SSE client disconnected", "session_id", sessionID)
            return
        }
    }
}

func (s *SSEServer) handleMessage(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
        return
    }

    sessionID := r.URL.Query().Get("session_id")

    var req JSONRPCRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "bad request", http.StatusBadRequest)
        return
    }

    // 处理请求,把响应通过 SSE channel 发回去
    // (真实实现需要修改 Server 使 writer 指向 SSE channel)
    // 这里简化为直接响应
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{
        "status":     "accepted",
        "session_id": sessionID,
    })
}

SSE 和 stdio 的选择原则:

  • 本地开发、单用户:用 stdio(更简单,Claude Desktop 默认支持)
  • 团队共享、服务器部署:用 SSE(多用户、可扩展)

第三部分:关键概念总结

概念 说明
MCP Protocol Anthropic 发布的开放标准,基于 JSON-RPC 2.0
stdio transport 通过 stdin/stdout 通信,适合本地工具
SSE transport 通过 HTTP + Server-Sent Events,适合网络服务
Tool Definition 包含名称、描述、InputSchema(JSON Schema 格式)
InputSchema 告诉 LLM 工具的参数结构,LLM 自动生成正确的调用
isError 工具返回错误时设为 true,LLM 会知道调用失败

为什么 MCP 用 JSON Schema 描述参数?

  • LLM 理解 JSON Schema
  • 可以验证 LLM 生成的参数是否合法
  • 和 OpenAI Function Calling 格式相似,降低学习成本

第四部分:自测清单

运行前,问自己:

  • MCP 解决了什么问题?(一处实现,多处复用)
  • stdio transport 的消息格式是什么?(每行一个 JSON-RPC 消息)
  • 为什么日志要写到 stderr 而不是 stdout?(stdout 被 MCP 占用)
  • tools/listtools/call 分别做什么?
  • Tool 的 InputSchema 有什么作用?(引导 LLM 生成正确参数)
  • isError: true 和 JSON-RPC 的 error 字段有什么区别?(前者是业务错误,后者是协议错误)

第五部分:作业

任务1:跑通 MCP Server

  • 构建 cmd/mcp/main.go
  • 手动发送 initialize + tools/list + tools/call 消息并验证输出
  • 确认日志输出在 stderr,JSON-RPC 响应在 stdout

任务2:接入 Claude Desktop

  • 配置 claude_desktop_config.json
  • 在 Claude Desktop 中验证工具出现
  • 让 Claude 调用 search_kb 回答一个问题

任务3:集成 Day 19 的 RBAC

  • MCP Server 在调用工具前检查调用方权限
  • 用环境变量传入 "当前用户 ID"(MCP_USER_ID
  • 没权限的工具调用返回 isError: true + 清晰的错误消息

任务4:扩展工具

  • 添加 list_tickets 工具:列出当前用户的工单
  • 添加 get_ticket_status 工具:查询单个工单状态
  • 为每个新工具定义正确的 InputSchema

任务5:深化思考

  • 如果 MCP Server 要支持认证(只有有 token 的 Client 才能连接),应该怎么做?
  • 工具的 description 怎么写才能让 LLM 更准确地调用? 试试不同的描述方式。
  • MCP 和 OpenAI Function Calling 有什么区别? 各适用于什么场景?

第六部分:常见问题

Q: MCP Server 挂了怎么办?Claude Desktop 会报什么错?

A: Claude Desktop 会显示工具不可用,聊天仍然可以继续(只是没有工具)。可以在 Server 里加健康检查和自动重启:

# 用 supervisor 或 systemd 保持 MCP Server 运行
# 或者简单的 shell 循环:
while true; do ./bin/mcp-server; sleep 1; done

Q: 如果工具执行时间很长(比如 10 秒),MCP 会超时吗?

A: MCP 没有强制超时,但 Claude Desktop 有自己的超时设置。建议:

  • 长耗时任务改为异步:工具调用立即返回一个 job_id,LLM 轮询状态
  • 或者在工具内部设置合理的 context timeout

Q: 一个 MCP Server 能注册多少个工具?

A: 协议上没有限制,但实践中建议 < 20 个。太多工具会让 LLM 困惑,也会消耗更多 context(每个工具的描述都会进 prompt)。分类管理:按功能域拆分成多个 MCP Server。

Q: 如何让 LLM 在合适的时机调用工具,而不是乱调用?

A: 好的工具描述 + 好的系统 prompt:

工具描述:具体说明"什么情况下用",而不只是"这个工具做什么" 系统 prompt:说明工具使用的边界条件

第七部分:算法练习

算法1:Word Search(DFS 回溯)

关联:MCP 的工具调用链可以看作图的遍历。Agent 收到任务后,可能需要按顺序调用多个工具,每步的结果决定下一步怎么走——类似在网格上搜索单词,不能走回头路。

// 单词搜索:在二维网格中找单词
// 输入:board=[["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word="ABCCED"
// 输出:true

func exist(board [][]byte, word string) bool {
    rows, cols := len(board), len(board[0])
    visited := make([][]bool, rows)
    for i := range visited {
        visited[i] = make([]bool, cols)
    }

    var dfs func(r, c, idx int) bool
    dfs = func(r, c, idx int) bool {
        if idx == len(word) {
            return true // 所有字符匹配成功
        }
        if r < 0 || r >= rows || c < 0 || c >= cols {
            return false
        }
        if visited[r][c] || board[r][c] != word[idx] {
            return false
        }

        visited[r][c] = true // 标记已访问

        // 向四个方向探索
        found := dfs(r+1, c, idx+1) ||
            dfs(r-1, c, idx+1) ||
            dfs(r, c+1, idx+1) ||
            dfs(r, c-1, idx+1)

        visited[r][c] = false // 回溯:取消标记
        return found
    }

    for r := 0; r < rows; r++ {
        for c := 0; c < cols; c++ {
            if dfs(r, c, 0) {
                return true
            }
        }
    }
    return false
}

复杂度:O(rows × cols × 4^L),L = word 长度


算法2:Number of Provinces(并查集)

关联:多个 MCP Server 可以互相调用(Server A 的工具内部调用 Server B)。Number of Provinces 用并查集统计有多少个独立的连通分量——类似判断你的 MCP 工具依赖图里有几个独立的服务簇。

// 省份数量:n 个城市,isConnected[i][j]=1 表示 i 和 j 直接相连
// 求省份数量(连通分量数)

func findCircleNum(isConnected [][]int) int {
    n := len(isConnected)
    parent := make([]int, n)
    for i := range parent {
        parent[i] = i // 初始:每个节点是自己的父节点
    }

    var find func(x int) int
    find = func(x int) int {
        if parent[x] != x {
            parent[x] = find(parent[x]) // 路径压缩
        }
        return parent[x]
    }

    union := func(x, y int) {
        px, py := find(x), find(y)
        if px != py {
            parent[px] = py
        }
    }

    // 合并有连接的城市
    for i := 0; i < n; i++ {
        for j := i + 1; j < n; j++ {
            if isConnected[i][j] == 1 {
                union(i, j)
            }
        }
    }

    // 统计根节点数量(即连通分量数)
    count := 0
    for i := 0; i < n; i++ {
        if find(i) == i {
            count++
        }
    }
    return count
}

算法3:Decode Ways(动态规划)

关联:MCP 工具的参数解析。当 LLM 生成工具调用参数时,一个模糊的输入可能有多种解析方式——类似数字编码的字符串有多少种解码方案。

// 解码方法:'A'=1, 'B'=2, ..., 'Z'=26
// 输入:"226",输出:3(BZ=226, VF=226不对,应是 "2 26"=BZ, "22 6"=BBF, "2 2 6"=BBF)
// 实际:226 → [2,2,6]→BBF, [2,26]→BZ, [22,6]→VF → 共3种

func numDecodings(s string) int {
    n := len(s)
    if n == 0 || s[0] == '0' {
        return 0
    }

    // dp[i] = s[:i] 的解码方案数
    dp := make([]int, n+1)
    dp[0] = 1 // 空字符串有1种解码
    dp[1] = 1 // 第一个字符(非'0')有1种解码

    for i := 2; i <= n; i++ {
        // 单个字符解码
        oneDigit := s[i-1] - '0'
        if oneDigit >= 1 {
            dp[i] += dp[i-1]
        }

        // 两个字符解码
        twoDigit := (int(s[i-2]-'0') * 10) + int(s[i-1]-'0')
        if twoDigit >= 10 && twoDigit <= 26 {
            dp[i] += dp[i-2]
        }
    }

    return dp[n]
}

下一步:Day 21 预告

明天是 Week 3 的集成日:

  1. 把 FSM + Checkpoint + Approval + Guardrails + RBAC + MCP 全部串联
  2. 跑通 End-to-End 测试:创建工单的完整流程(权限 → 工具调用 → MCP → 审批)
  3. 准备"设计企业 Agent 平台"的系统设计面试答案

准备问题:

  • 你能不看代码,把今天的 MCP Server 画出架构图吗?
  • 如果要在生产环境部署 MCP Server,需要考虑哪些非功能性需求?(高可用、监控、认证)