golang mcp 服务端指南

07-注册预置提示词

读完这篇你能:把一份整理好的 prompt 注册到服务端,让客户端直接取用。

前置:提供 Resources 知识库。

为什么要预置提示词

主线案例上线后,日常用法是这样:在 Cursor 里让模型去查日志、分析原因。

日常用法

但很快会发现,错误信息一复杂,模型自主分析的效果就飘。于是你总结了一份分析套路,写成了 prompt——就是这份模板。

一开始是每次手动粘贴。等同事也遇到同样的问题、一堆人来找你要模板时,就该把它注册到服务端了:谁要用谁自己拉,不用再手动粘。

顺便厘清一个容易误解的点:预置提示词是「快速生成一段提示词」,不是让 AI 自主决定调用。所以客户端里它是用户主动选的一个动作,不是模型自己挑的工具。

完整代码

// MCP Prompt 示例:统一错误分析。
// 提示词正文来自同目录的 error_analysis_prompt.md,编译时嵌入,运行时渲染。
package main

import (
	"context"
	"embed"
	"fmt"
	"log"
	"net/http"
	"strings"
	"text/template"

	"github.com/modelcontextprotocol/go-sdk/mcp"
)

//go:embed error_analysis_prompt.md
var promptFS embed.FS

var errorAnalysisTmpl = template.Must(
	template.New("error_analysis_prompt.md").ParseFS(promptFS, "error_analysis_prompt.md"),
)

type errorAnalysisPromptData struct {
	ServiceName string
	TimeRange   string
}

func promptHandler(_ context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
	args := req.Params.Arguments
	data := errorAnalysisPromptData{
		ServiceName: strings.TrimSpace(args["service_name"]),
		TimeRange:   strings.TrimSpace(args["time_range"]),
	}

	var buf strings.Builder
	if err := errorAnalysisTmpl.Execute(&buf, data); err != nil {
		return nil, fmt.Errorf("渲染提示词模板: %w", err)
	}

	return &mcp.GetPromptResult{
		Description: "错误分析:根据服务名和时间范围,自动查日志并分析根因",
		Messages: []*mcp.PromptMessage{
			{Role: "user", Content: &mcp.TextContent{Text: strings.TrimSpace(buf.String()) + "\n"}},
		},
	}, nil
}

func main() {
	s := mcp.NewServer(&mcp.Implementation{Name: "log-mcp", Version: "0.1.0"}, nil)

	// 工具定义见 02-工具的参数.md
	mcp.AddTool(s, &mcp.Tool{
		Name:        "list_services",
		Description: "列出所有服务名",
	}, listServicesTool)
	mcp.AddTool(s, &mcp.Tool{
		Name:        "query_logs",
		Description: "按服务名和时间范围查询日志,可筛选日志级别",
		InputSchema: queryLogsInputSchema,
	}, queryLogsTool)

	// 注册预置提示词
	s.AddPrompt(&mcp.Prompt{
		Name:        "error-analysis",
		Description: "错误分析:根据服务名和时间范围,自动查日志并分析根因",
		Arguments: []*mcp.PromptArgument{
			{Name: "service_name", Description: "服务名称"},
			{Name: "time_range", Description: `时间范围,如 "30m"、"1h" 或 "start/end"`},
		},
	}, promptHandler)

	handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return s }, nil)
	if err := http.ListenAndServe("127.0.0.1:8000", handler); err != nil {
		log.Fatal(err)
	}
}

// --- list_services 的实现放这里;query_logs 见 02-工具的参数.md,不重复 ---
type listServicesOutput struct {
	Services []string `json:"services" jsonschema:"服务名列表"`
}

func listServicesTool(_ context.Context, _ *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, listServicesOutput, error) {
	return nil, listServicesOutput{Services: []string{"billing-service", "order-service"}}, nil
}

接口调试里能看到这个 prompt 和它的参数:

在接口调试工具里调用 prompt

在 Cursor 里使用

添加:AddPrompt

s.AddPrompt(&mcp.Prompt{
	Name:        "error-analysis",
	Description: "错误分析:根据服务名和时间范围,自动查日志并分析根因",
	Arguments: []*mcp.PromptArgument{
		{Name: "service_name", Description: "服务名称"},
		{Name: "time_range", Description: `时间范围,如 "30m"、"1h" 或 "start/end"`},
	},
}, promptHandler)

注册的字段和工具几乎一样:

字段 作用
Name 唯一标识,客户端靠它调用
Description 让客户端和用户知道这个 prompt 是干什么的
Arguments 声明模板需要哪些参数

参数默认是可选的;必填的设 Required: true。

注意函数归属不同:添加工具是 mcp.AddTool(s, …),添加提示词是 s.AddPrompt(…)——一个在包上,一个在 server 实例上。这个不对称容易被记混。

提示词正文:单独一个文件

模板用的是 Go 自带的 text/template,正文存放在独立的 markdown 文件里,通过 //go:embed 在编译时打进二进制。这样正文可以用 markdown 高亮和预览,不用挤在字符串里。

//go:embed error_analysis_prompt.md
var promptFS embed.FS

var errorAnalysisTmpl = template.Must(
	template.New("error_analysis_prompt.md").ParseFS(promptFS, "error_analysis_prompt.md"),
)

go:embed 的细节见 go:embed 速查。用 %s 占位符直接拼字符串当然也行,模板只是让分支和循环好写一点。

正文内容见分析日志 Prompt 模板。

handler 返回什么

func promptHandler(_ context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error)

入参 *mcp.GetPromptRequest 里只有两样东西:

  • Params.Name——客户端要的是哪个 prompt
  • Params.Arguments——模板参数,键值对,值都是字符串

返回值里 Description 可以补一句说明,核心是 Messages——展开后的对话消息列表,可以多条、可以多角色。

PromptMessage 只有两个字段:

字段 作用
Role 说话方角色,协议上为 user 或 assistant
Content 单条消息的内容块,类型由 JSON 的 type 字段区分

Content 是个公共定义,工具结果、资源内容、提示词消息都用它,Content 里列了全部七种。

一个技巧:在提示词里附一张图

提示词正文是纯文本,但 Messages 可以塞多段内容。比如附一张分析流程图,让模型按固定流程走:

分析流程图

//go:embed error_analysis_flowchart.png
var errorAnalysisFlowchartPNG []byte
return &mcp.GetPromptResult{
	Description: "错误分析:根据服务名和时间范围,自动查日志并分析根因",
	Messages: []*mcp.PromptMessage{
		{Role: "user", Content: &mcp.TextContent{Text: text}},
		{
			Role: "user",
			Content: &mcp.ImageContent{
				MIMEType: "image/png",
				Data:     errorAnalysisFlowchartPNG,
			},
		},
	},
}, nil

模型收到图和文字后,会更容易按图上的步骤走:

效果

MIMEType 的取值见 MIME 类型。


下一篇:错误返回与重试——分清协议错误和业务错误。