公告

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


Skip to content

引导(Elicitation)

了解如何实现可以在交互期间向用户请求其他信息的 MCP 服务器。

概述

引出允许服务器在交互期间向用户请求其他信息。当工具或操作需要用户澄清、确认或原始请求中未提供的额外输入时,这很有用。

有两种引出模式:

  • 表单模式mcp.ElicitationModeForm)— 服务器提供 JSON Schema,客户端向用户显示结构化表单。这是默认模式。
  • URL 模式mcp.ElicitationModeURL)— 服务器提供用户访问的 URL,实现带外交互(如身份验证流程)。

::::info[需要用户同意] 引出本质上是面向用户的。当您的服务器发送引出请求时:

  • 客户端将向用户显示请求
  • 用户可以接受、拒绝或取消
  • 响应时间取决于用户交互

相应设计您的工具:

  • 提供清晰、人类可读的消息解释您需要什么以及为什么
  • 优雅地处理所有三种响应操作(接受、拒绝、取消)
  • 考虑用户交互延迟的超时 :::

启用引出

要在服务器中启用引出,请在服务器创建期间使用 WithElicitation() 选项:

go
package main

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

func main() {
    // 创建带有引出能力的服务器
    s := server.NewMCPServer("Elicitation Server", "1.0.0",
        server.WithElicitation(),
    )

    // 添加使用引出的工具...

    // 启动服务器
    server.ServeStdio(s)
}

ElicitationParams

ElicitationParams 结构定义服务器向用户请求的内容:

go
type ElicitationParams struct {
    Meta            *Meta  `json:"_meta,omitempty"`
    Mode            string `json:"mode,omitempty"`            // "form" 或 "url",默认为 "form"
    Message         string `json:"message"`                   // 人类可读的消息

    // 表单模式字段
    RequestedSchema any    `json:"requestedSchema,omitempty"` // 表单的 JSON Schema

    // URL 模式字段
    ElicitationID   string `json:"elicitationId,omitempty"`   // 用于跟踪的唯一 ID
    URL             string `json:"url,omitempty"`             // 用户访问的 URL
}

验证

ElicitationParams 有一个 Validate() 方法,强制执行特定于模式的 requirements:

  • 表单模式要求 RequestedSchema 非空
  • URL 模式要求同时设置 ElicitationIDURL

使用 RequestElicitation() 时会自动调用验证。

响应操作

当用户响应引出请求时,结果包含三种操作之一:

go
mcp.ElicitationResponseActionAccept   // 用户提供了请求的信息
mcp.ElicitationResponseActionDecline  // 用户明确拒绝
mcp.ElicitationResponseActionCancel   // 用户取消而未做选择

结果作为 ElicitationResult 返回:

go
type ElicitationResult struct {
    Result
    ElicitationResponse
}

type ElicitationResponse struct {
    Action  ElicitationResponseAction `json:"action"`
    Content any                       `json:"content,omitempty"` // 用户的响应数据
}

Actionaccept 时,Content 字段包含用户的响应数据,应符合原始请求中的 RequestedSchema

表单模式

表单模式是默认模式。服务器提供描述预期输入的 JSON Schema,客户端为用户呈现表单。

在工具处理程序中请求引出

在工具处理程序中使用 RequestElicitation() 向用户请求信息:

go
mcpServer.AddTool(
    mcp.NewTool(
        "create_project",
        mcp.WithDescription("使用用户指定的配置创建新项目"),
    ),
    func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        // 使用 JSON Schema 构建引出请求
        elicitationRequest := mcp.ElicitationRequest{
            Params: mcp.ElicitationParams{
                Message: "请提供项目详情。",
                RequestedSchema: map[string]any{
                    "type": "object",
                    "properties": map[string]any{
                        "projectName": map[string]any{
                            "type":        "string",
                            "description": "项目名称",
                            "minLength":   1,
                        },
                        "framework": map[string]any{
                            "type":        "string",
                            "description": "要使用的前端框架",
                            "enum":        []string{"react", "vue", "angular", "none"},
                        },
                        "includeTests": map[string]any{
                            "type":        "boolean",
                            "description": "包含测试设置",
                            "default":     true,
                        },
                    },
                    "required": []string{"projectName"},
                },
            },
        }

        // 发送引出请求
        result, err := mcpServer.RequestElicitation(ctx, elicitationRequest)
        if err != nil {
            return nil, fmt.Errorf("failed to request elicitation: %w", err)
        }

        // 处理用户的响应
        switch result.Action {
        case mcp.ElicitationResponseActionAccept:
            data, ok := result.Content.(map[string]any)
            if !ok {
                return nil, fmt.Errorf("unexpected response format")
            }

            projectName := data["projectName"].(string)
            framework := "none"
            if fw, ok := data["framework"].(string); ok {
                framework = fw
            }

            message := fmt.Sprintf("Created project '%s' with framework: %s", projectName, framework)
            return &mcp.CallToolResult{
                Content: []mcp.Content{
                    mcp.NewTextContent(message),
                },
            }, nil

        case mcp.ElicitationResponseActionDecline:
            return &mcp.CallToolResult{
                Content: []mcp.Content{
                    mcp.NewTextContent("项目创建已取消 — 用户拒绝。"),
                },
            }, nil

        case mcp.ElicitationResponseActionCancel:
            return nil, fmt.Errorf("project creation cancelled by user")

        default:
            return nil, fmt.Errorf("unexpected response action: %s", result.Action)
        }
    },
)

条件引出

您可以仅在需要时请求引出——例如,确认潜在的危险操作:

go
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    data, _ := request.RequireString("data")

    // 仅在数据较大时请求确认
    if len(data) > 100 {
        result, err := mcpServer.RequestElicitation(ctx, mcp.ElicitationRequest{
            Params: mcp.ElicitationParams{
                Message: fmt.Sprintf("数据为 %d 个字符。是否继续?", len(data)),
                RequestedSchema: map[string]any{
                    "type": "object",
                    "properties": map[string]any{
                        "proceed": map[string]any{
                            "type":        "boolean",
                            "description": "是否继续处理",
                        },
                    },
                    "required": []string{"proceed"},
                },
            },
        })
        if err != nil {
            return nil, fmt.Errorf("failed to get confirmation: %w", err)
        }

        if result.Action != mcp.ElicitationResponseActionAccept {
            return mcp.NewToolResultText("处理已被用户取消。"), nil
        }

        responseData := result.Content.(map[string]any)
        if proceed, ok := responseData["proceed"].(bool); !ok || !proceed {
            return mcp.NewToolResultText("用户选择不继续。"), nil
        }
    }

    // 继续处理...
    return mcp.NewToolResultText(fmt.Sprintf("已处理 %d 个字符", len(data))), nil
}

URL 模式

URL 模式用于用户需要访问外部 URL 的带外交互——例如 OAuth 流程或 API 密钥设置页面。

请求 URL 引出

使用 RequestURLElicitation() 方便地发送 URL 模式请求:

go
mcpServer.AddTool(
    mcp.NewTool(
        "auth_via_url",
        mcp.WithDescription("通过浏览器进行身份验证"),
    ),
    func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        session := server.ClientSessionFromContext(ctx)
        if session == nil {
            return nil, fmt.Errorf("no active session")
        }

        // 生成用于跟踪的唯一引出 ID
        elicitationID := uuid.New().String()
        url := fmt.Sprintf("https://myserver.com/auth?id=%s", elicitationID)

        // 发送 URL 引出请求
        result, err := mcpServer.RequestURLElicitation(
            ctx,
            session,
            elicitationID,
            url,
            "请在浏览器中进行身份验证以继续。",
        )
        if err != nil {
            return nil, fmt.Errorf("URL elicitation failed: %w", err)
        }

        if result.Action == mcp.ElicitationResponseActionAccept {
            return &mcp.CallToolResult{
                Content: []mcp.Content{
                    mcp.NewTextContent("身份验证流程已启动。请在浏览器中完成流程。"),
                },
            }, nil
        }

        return &mcp.CallToolResult{
            Content: []mcp.Content{
                mcp.NewTextContent(fmt.Sprintf("用户拒绝身份验证:%s", result.Action)),
            },
        }, nil
    },
)

完成通知

对于 URL 模式,一旦用户完成带外流程(例如,在浏览器中提交表单,服务器收到回调),通知客户端引出已完成:

go
// 在服务器收到浏览器回调后发送
err := mcpServer.SendElicitationComplete(ctx, session, elicitationID)
if err != nil {
    log.Printf("Failed to send completion notification: %v", err)
}

您也可以直接构造通知:

go
notification := mcp.NewElicitationCompleteNotification("elicitation-id-123")

URLElicitationRequiredError

当工具需要尚未设置的身份验证时,您可以返回 URLElicitationRequiredError 以发出信号,告知客户端应启动 URL 引出流程:

go
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    if !isAuthorized(ctx) {
        elicitationID := uuid.New().String()
        return nil, mcp.URLElicitationRequiredError{
            Elicitations: []mcp.ElicitationParams{
                {
                    Mode:          mcp.ElicitationModeURL,
                    ElicitationID: elicitationID,
                    URL:           fmt.Sprintf("https://myserver.com/authorize?id=%s", elicitationID),
                    Message:       "需要授权才能访问此资源。",
                },
            },
        }
    }

    return mcp.NewToolResultText("操作成功完成!"), nil
}

客户端处理程序

客户端实现 ElicitationHandler 接口来处理服务器的引出请求:

go
type ElicitationHandler interface {
    Elicit(ctx context.Context, request mcp.ElicitationRequest) (*mcp.ElicitationResult, error)
}

创建客户端时注册处理程序:

go
import "github.com/mark3labs/mcp-go/client"

handler := &MyElicitationHandler{}
c := client.NewClient(transport, client.WithElicitationHandler(handler))

实现应:

  1. 向用户显示请求消息(以及 URL 模式下的 URL)
  2. 根据请求的 schema 验证输入(表单模式)
  3. 允许用户接受、拒绝或取消
  4. 返回适当的 ElicitationResult

ElicitationCapability

ElicitationCapability 结构在初始化期间公布客户端或服务器支持哪种引出模式:

go
type ElicitationCapability struct {
    Form *struct{} `json:"form,omitempty"` // 支持表单模式
    URL  *struct{} `json:"url,omitempty"`  // 支持 URL 模式
}

此能力作为 ClientCapabilitiesServerCapabilities 的一部分在 MCP 初始化握手期间交换。

错误处理

始终优雅地处理引出错误和所有响应操作:

go
result, err := mcpServer.RequestElicitation(ctx, elicitationRequest)
if err != nil {
    // 常见错误:
    // - server.ErrNoActiveSession: 上下文中没有会话
    // - server.ErrElicitationNotSupported: 会话/传输不支持引出
    // - ElicitationParams.Validate() 的验证错误
    log.Printf("Elicitation failed: %v", err)
    return mcp.NewToolResultError(fmt.Sprintf("Could not request information: %v", err)), nil
}

上下文和超时

使用上下文进行超时控制,特别是因为引出需要用户交互:

go
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()

result, err := mcpServer.RequestElicitation(ctx, elicitationRequest)

最佳实践

  1. 清晰的消息:编写人类可读的消息,解释您需要什么以及为什么
  2. 最小的 Schema:仅请求您实际需要的信息
  3. 处理所有操作:始终处理接受、拒绝和取消——不要假设用户会接受
  4. 验证响应:使用前对 Content 字段进行类型断言和验证
  5. 优雅降级:如果引出失败或不受支持,提供合理的回退
  6. 谨慎使用 URL 模式:仅为真正需要浏览器的流程保留 URL 模式(如 OAuth)
  7. 跟踪引出 ID:对于 URL 模式,使用唯一 ID 将浏览器回调与会话关联

下一步