公告

任何建议或需求可联系我!


Skip to content

采样

学习如何实现能够通过采样功能向客户端请求LLM补全的MCP服务器,

概述

采样允许 MCP 服务器从客户端请求 LLM 完成,实现双向通信,服务器可以利用客户端的 LLM 功能。这对于需要生成内容、回答问题或执行推理任务的工具特别有用。

[需要用户同意]

根据 MCP 规范,客户端 应该 为采样请求实现人工审核批准。

当您从客户端请求采样时:

  • 用户通常会被提示审查和批准您的请求
  • 用户可以在发送到 LLM 之前修改您的提示
  • 用户可以完全拒绝您的请求
  • 由于用户交互,响应时间可能更长

相应设计您的工具:

  • 提供清晰的描述说明为什么需要采样
  • 使用描述性系统提示解释目的
  • 优雅地处理拒绝错误
  • 考虑用户批准延迟的超时
  • 不要假设立即或自动批准

精心设计的采样请求可以提高用户信任度和批准率。 :::

启用采样

要在服务器中启用采样,请在服务器设置期间调用 EnableSampling()

go
package main

import (
    "context"
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func main() {
    // 创建服务器
    mcpServer := server.NewMCPServer("my-server", "1.0.0")
    
    // 启用采样能力
    mcpServer.EnableSampling()
    
    // 添加使用采样的工具...
    
    // 启动服务器
    server.ServeStdio(mcpServer)
}

请求采样

在工具处理程序中使用 RequestSampling() 请求 LLM 完成:

go
mcpServer.AddTool(mcp.Tool{
    Name:        "ask_llm",
    Description: "使用采样向 LLM 提问",
    InputSchema: mcp.ToolInputSchema{
        Type: "object",
        Properties: map[string]any{
            "question": map[string]any{
                "type":        "string",
                "description": "要问 LLM 的问题",
            },
            "system_prompt": map[string]any{
                "type":        "string", 
                "description": "可选的系统提示",
            },
        },
        Required: []string{"question"},
    },
}, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // 提取参数
    question, err := request.RequireString("question")
    if err != nil {
        return nil, err
    }
    
    systemPrompt := request.GetString("system_prompt", "You are a helpful assistant.")
    
    // 创建采样请求
    samplingRequest := mcp.CreateMessageRequest{
        CreateMessageParams: mcp.CreateMessageParams{
            Messages: []mcp.SamplingMessage{
                {
                    Role: mcp.RoleUser,
                    Content: mcp.TextContent{
                        Type: "text",
                        Text: question,
                    },
                },
            },
            SystemPrompt: systemPrompt,
            MaxTokens:    1000,
            Temperature:  0.7,
        },
    }
    
    // 从客户端请求采样
    result, err := mcpServer.RequestSampling(ctx, samplingRequest)
    if err != nil {
        return &mcp.CallToolResult{
            Content: []mcp.Content{
                mcp.TextContent{
                    Type: "text",
                    Text: fmt.Sprintf("Error requesting sampling: %v", err),
                },
            },
            IsError: true,
        }, nil
    }
    
    // 返回 LLM 响应
    return &mcp.CallToolResult{
        Content: []mcp.Content{
            mcp.TextContent{
                Type: "text",
                Text: fmt.Sprintf("LLM Response: %s", getTextFromContent(result.Content)),
            },
        },
    }, nil
})

采样请求参数

CreateMessageRequest 支持各种参数来控制 LLM 行为:

go
samplingRequest := mcp.CreateMessageRequest{
    CreateMessageParams: mcp.CreateMessageParams{
        // 必需:发送到 LLM 的消息
        Messages: []mcp.SamplingMessage{
            {
                Role: mcp.RoleUser,        // 或 mcp.RoleAssistant
                Content: mcp.TextContent{   // 或 mcp.ImageContent
                    Type: "text",
                    Text: "Your message here",
                },
            },
        },

        // 可选:上下文的系统提示
        SystemPrompt: "You are a helpful assistant.",

        // 可选:生成的最大令牌数
        MaxTokens: 1000,

        // 可选:随机性温度(0.0 到 1.0)
        Temperature: 0.7,

        // 可选:停止序列
        StopSequences: []string{"\\n\\n"},

        // 可选(2025-11-25):上下文包含提示。仅在客户端公布 `sampling.context` 时才发送
        // "none" 以外的值 — 请参阅下面的[能力门控](#capability-gating-2025-11-25)。
        IncludeContext: "thisServer",

        // 可选(2025-11-25):模型在生成期间可以使用的工具,以及
        // 模型应该多积极地使用它们。仅在
        // 客户端公布了 `sampling.tools` 时才发送这些。
        Tools: []mcp.Tool{ /* ... */ },
        ToolChoice: &mcp.ToolChoice{Mode: mcp.ToolChoiceModeAuto},
    },
}

能力门控(2025-11-25)

2025-11-25 协议修订将采样分为基线能力和两个选择性加入的子能力。在发送门控字段之前,检查连接会话的 ClientCapabilities.Sampling — 符合规范的客户端必须拒绝违反下表中规则的请求。

CreateMessageParams 上的字段必需的客户端子能力子能力为 nil
IncludeContext("none" 以外的值)Sampling.Context省略字段或发送 "none"
ToolsSampling.Tools省略字段
ToolChoiceSampling.Tools省略字段

工具处理程序内的典型模式:

go
func askLLMWithTools(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    session := server.ClientSessionFromContext(ctx)
    sessionWithClient, ok := session.(server.SessionWithClientInfo)
    if !ok {
        return mcp.NewToolResultError("client session is not capability-aware"), nil
    }
    caps := sessionWithClient.GetClientCapabilities()

    params := mcp.CreateMessageParams{
        Messages:  []mcp.SamplingMessage{ /* ... */ },
        MaxTokens: 1000,
    }

    // 仅在客户端选择加入时附加 Tools / ToolChoice。
    if caps.Sampling != nil && caps.Sampling.Tools != nil {
        params.Tools = []mcp.Tool{ /* ... */ }
        params.ToolChoice = &mcp.ToolChoice{Mode: mcp.ToolChoiceModeAuto}
    }

    // 仅在客户端选择加入时请求上下文包含。
    if caps.Sampling != nil && caps.Sampling.Context != nil {
        params.IncludeContext = "thisServer"
    }

    result, err := mcpServer.RequestSampling(ctx, mcp.CreateMessageRequest{CreateMessageParams: params})
    // ...
}

ToolChoiceMode 接受三个常量:

常量传输值含义
mcp.ToolChoiceModeAuto"auto"模型决定(省略 ToolChoice 时的默认值)。
mcp.ToolChoiceModeRequired"required"模型必须调用至少一个工具。
mcp.ToolChoiceModeNone"none"此请求禁用工具使用。

消息类型

采样支持不同的消息角色和内容类型:

消息角色

go
// 用户消息
{
    Role: mcp.RoleUser,
    Content: mcp.TextContent{
        Type: "text",
        Text: "法国的首都是哪里?",
    },
}

// 助手消息(用于对话上下文)
{
    Role: mcp.RoleAssistant,
    Content: mcp.TextContent{
        Type: "text", 
        Text: "法国的首都是巴黎。",
    },
}

内容类型

文本内容

go
mcp.TextContent{
    Type: "text",
    Text: "Your text content here",
}

图像内容

go
mcp.ImageContent{
    Type: "image",
    Data: "base64-encoded-image-data",
    MimeType: "image/jpeg",
}

工具使用和工具结果内容(2025-11-25)

当采样涉及工具时,对话中的中间消息还可以携带 ToolUseContent(模型要求调用工具)和 ToolResultContent(该调用的结果)—— 这些通过相同的 SamplingMessage.Content 字段流动:

go
// 助手请求工具调用
mcp.ToolUseContent{
    Type:  "tool_use",
    ID:    "tu_1",
    Name:  "get_weather",
    Input: map[string]any{"city": "London"},
}

// 携带工具输出的用户角色消息
mcp.ToolResultContent{
    Type:      "tool_result",
    ToolUseID: "tu_1",
    Content:   []mcp.Content{mcp.NewTextContent("Sunny, 22°C")},
}

使用构造函数 mcp.NewToolUseContentmcp.NewToolResultContent 来构建这些,而无需手动编写 Type 判别器。

错误处理

始终优雅地处理采样错误:

go
result, err := mcpServer.RequestSampling(ctx, samplingRequest)
if err != nil {
    // 记录错误
    log.Printf("Sampling request failed: %v", err)
    
    // 返回适当的错误响应
    return &mcp.CallToolResult{
        Content: []mcp.Content{
            mcp.TextContent{
                Type: "text",
                Text: "抱歉,此时无法处理您的请求。",
            },
        },
        IsError: true,
    }, nil
}

上下文和超时

使用上下文进行超时控制:

go
// 为采样请求设置超时
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

result, err := mcpServer.RequestSampling(ctx, samplingRequest)

最佳实践

  1. 尽早启用采样:在服务器初始化期间调用 EnableSampling()
  2. 处理超时:为采样请求设置适当的超时
  3. 优雅错误:始终向用户提供有意义的错误消息
  4. 内容提取:使用辅助函数从响应中提取文本
  5. 系统提示:使用清晰的系统提示来指导 LLM 行为
  6. 参数验证:在发出采样请求之前验证工具参数

完整示例

这是一个带有采样的完整示例服务器:

go
package main

import (
    "context"
    "fmt"
    "log"
    "time"
    
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func main() {
    // 创建服务器
    mcpServer := server.NewMCPServer("sampling-example-server", "1.0.0")
    
    // 启用采样能力
    mcpServer.EnableSampling()
    
    // 添加采样工具
    mcpServer.AddTool(mcp.Tool{
        Name:        "ask_llm",
        Description: "使用采样向 LLM 提问",
        InputSchema: mcp.ToolInputSchema{
            Type: "object",
            Properties: map[string]any{
                "question": map[string]any{
                    "type":        "string",
                    "description": "要问 LLM 的问题",
                },
                "system_prompt": map[string]any{
                    "type":        "string",
                    "description": "可选的系统提示",
                },
            },
            Required: []string{"question"},
        },
    }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        question, err := request.RequireString("question")
        if err != nil {
            return nil, err
        }
        
        systemPrompt := request.GetString("system_prompt", "You are a helpful assistant.")
        
        // 创建采样请求
        samplingRequest := mcp.CreateMessageRequest{
            CreateMessageParams: mcp.CreateMessageParams{
                Messages: []mcp.SamplingMessage{
                    {
                        Role: mcp.RoleUser,
                        Content: mcp.TextContent{
                            Type: "text",
                            Text: question,
                        },
                    },
                },
                SystemPrompt: systemPrompt,
                MaxTokens:    1000,
                Temperature:  0.7,
            },
        }
        
        // 使用超时请求采样
        samplingCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
        defer cancel()
        
        result, err := mcpServer.RequestSampling(samplingCtx, samplingRequest)
        if err != nil {
            return &mcp.CallToolResult{
                Content: []mcp.Content{
                    mcp.TextContent{
                        Type: "text",
                        Text: fmt.Sprintf("Error requesting sampling: %v", err),
                    },
                },
                IsError: true,
            }, nil
        }
        
        // 返回 LLM 响应
        return &mcp.CallToolResult{
            Content: []mcp.Content{
                mcp.TextContent{
                    Type: "text",
                    Text: fmt.Sprintf("LLM Response (model: %s): %s", 
                        result.Model, getTextFromContent(result.Content)),
                },
            },
        }, nil
    })
    
    // 启动服务器
    log.Println("Starting sampling example server...")
    if err := server.ServeStdio(mcpServer); err != nil {
        log.Fatalf("Server error: %v", err)
    }
}

// 用于从内容中提取文本的辅助函数
func getTextFromContent(content interface{}) string {
    switch c := content.(type) {
    case mcp.TextContent:
        return c.Text
    case string:
        return c
    default:
        return fmt.Sprintf("%v", content)
    }
}

传输支持

采样支持以下传输:

STDIO 传输

STDIO 传输通过 JSON-RPC 消息传递提供完整的采样支持:

go
// 使用采样启动 STDIO 服务器
server.ServeStdio(mcpServer)

客户端必须在初始化期间实现 SamplingHandler 并公布采样能力。

进程内传输

进程内传输提供最有效的采样实现,具有直接方法调用:

go
// 使用采样处理程序创建进程内客户端
mcpClient, err := client.NewInProcessClientWithSamplingHandler(mcpServer, samplingHandler)
if err != nil {
    log.Fatal(err)
}

进程内采样的优势:

  • 直接方法调用:无 JSON-RPC 序列化开销
  • 类型安全:编译时类型检查

不支持的传输

以下传输目前不支持采样:

  • SSE 传输:单向流特性阻止双向采样
  • StreamableHTTP 传输:无状态 HTTP 请求不支持采样回调

对于这些传输,考虑直接在工具处理程序中实现 LLM 集成,而不是使用采样。

下一步