模型上下文协议(Model Context Protocol,简称 MCP)是一种开放标准,旨在标准化大型语言模型(LLM)与外部数据源和工具之间的交互方式。 由 Anthropic 于 2024 年 11 月推出,MCP 通过定义统一的接口,使 AI 应用能够安全、灵活地访问和操作本地及远程数据资源,提升模型的功能性和可扩展性。
MCP 遵循 CS 架构(client-server),具体包含的组件如下:
Host 主机:发起连接 LLM 的应用程序,例如 Claude for Desktop 或其他的 AI 应用。
MCP Client 客户端:运行在主机里的客户端,与 MCP Server 服务器保持 1:1 连接,负责协议通信。
MCP Server 服务器:负责向客户端提供 资源、提示 和 工具 的服务器。
Resources(资源)是 MCP
协议中的核心原语之一,服务器通过它可以向客户端提供可读的数据或内容,用作 LLM
交互的上下文信息。每个资源通过 URI
进行标识,格式如下:
[协议]://[主机]/[路径]
示例:
file:///home/www/documents/go.pdf
postgres://database/www/schema
screen://localhost/www/display1
示例:
// 静态资源示例
resource := mcp.NewResource(
"config://settings",
"应用配置信息",
mcp.WithResourceDescription("获取当前应用的配置参数"),
mcp.WithMIMEType("application/json"),
)
// 动态资源示例
userProfileTemplate := mcp.NewResourceTemplate(
"user://profile/{userId}",
"用户资料",
mcp.WithTemplateDescription("获取指定用户的详细信息"),
)
资源类型:
**文本类型:**包含 UTF_8
编码的文本数据,例如:文本、代码、日志等等
**二进制资源:**包含 Base64
编码的原始二进制数据,例如:图片、PDF文件、音视频等等
客户端可以通过调用服务器的 resources/list
获取资源列表:
{
"uri": "string", // 资源唯一标识符
"name": "string", // 资源名称(人类可读)
"description": "string", // 可选,资源描述
"mimeType": "string" // 可选,资源 MIME 类型
}
客户还可通过 resources/read
接口读取资源内容,只需传入资源的 URI
。返回结构如下:
{
contents: [
{
uri: string; // 资源唯一标识符
mimeType?: string; // 可选,资源 MIME 类型
// 以下二选一
text?: string; // 文本资源
blob?: string; // 二进制资源
}
]
}
Tools
(工具) 是 MCP
协议中的一项关键原语,服务器可通过它向客户端暴露可执行功能,供 LLM
使用(通常需要用户批准,确保人类参与决策)。Tools
的核心概念包括:
发现(Discovery):客户端可通过 tools/list
接口获取可用工具列表。
调用(Invocation):客户端可通过 tools/call
接口发起工具调用请求,由服务器执行具体操作并返回结果。
灵活性(Flexibility):工具既可以是简单的计算函数,也可以是复杂的 API
集成。
tools结构定义:
{
"name": "string", // 工具唯一标识符
"description": "string", // 可选,工具描述
"inputSchema": { // 工具参数的 JSON Schema
"type": "object",
"properties": { ... } // 工具参数定义
}
}
使用go代码实例化定义:
// 计算工具
calculatorTool := mcp.NewTool("calculate",
mcp.WithDescription("执行数学计算"),
mcp.WithString("expression", mcp.Required(),
mcp.Description("数学表达式,如 '2+3*4'")),
mcp.WithString("format",
mcp.Description("结果格式: decimal, fraction, scientific")),
)
// 文件操作工具
fileWriteTool := mcp.NewTool("write_file",
mcp.WithDescription("写入文件内容"),
mcp.WithString("path", mcp.Required()),
mcp.WithString("content", mcp.Required()),
mcp.WithBoolean("append", mcp.Description("是否追加模式")),
)
提示词 是服务器定义可复用的提示词模板和工作流,客户端可以轻松将这些模板呈现给用户或 LLM
。
结构:
{
"name": "string", // 提示词唯一标识符
"description": "string", // 可选,人类可读的描述
"arguments": [ // 可选参数列表
{
"name": "string", // 参数标识符
"description": "string", // 可选,参数描述
"required": "boolean" // 是否为必填参数
}
]
}
使用go代码实例化定义:
codeReviewPrompt := mcp.NewPrompt("code_review",
mcp.WithPromptDescription("代码审查模板"),
mcp.WithPromptArgument("language", "编程语言"),
mcp.WithPromptArgument("code", "要审查的代码"),
)
客户端可以通过调用 prompts/list
获取可用的提示词列表:
{
"prompts": [
{
"name": "code_review", // 提示词唯一标识符
"description": "代码审查模板", // 提示词描述
"arguments": [
{
"name": "language", // 参数名称
"description": "编程语言", // 参数描述
"required": true // 是否必填
}
]
}
]
}
Prompts(提示词)、工具(Tool)、资源(Resource)形成互补
Tool 解决“执行动作”的问题;
Resource 提供“静态/动态文件”或长文本;
Prompt 则专注于“对话内容模板”
因此在一个常见工作流中,可以:
通过 Tool 计算、抓取或生成某些数据;
把数据保存为文件或直接包装成 ResourceContents 由资源回调返回;
让 Prompt 引用这个资源 URI,把内容作为上下文发给模型。
MCP
协议内置了两种标准传输方式:标准输入/输出(stdio) 和 Server-Sent Events(SSE) 、Streamable HTTP。
stdio
传输通过 标准输入输出流 实现客户端与服务器之间的通信,适用于本地集成与命令行工具。
推荐在以下场景使用 stdio
:
构建命令行工具
本地系统集成
简单进程间通信
与 shell
脚本协作
Go server
示例:
s := server.NewMCPServer(
"My Server", // Server name
"1.0.0", // Version
)
if err := server.ServeStdio(s); err != nil {
log.Fatalf("Server error: %v", err)
}
SSE
传输 通过 HTTP POST
请求实现 客户端到服务器通信,同时支持 服务器到客户端流式传输 。
推荐在以下场景使用 SSE
:
仅需要服务器到客户端的流式通信
运行在受限网络环境
实现简单的推送更新
Go server
示例:
s := server.NewMCPServer(
"My Server", // Server name
"1.0.0", // Version
)
sseServer := server.NewSSEServer(s)
err := sseServer.Start(":8080")
if err != nil {
panic(err)
}
2025 年 3 月 26 日,MCP引入了一项关键更新:用 Streamable HTTP 替代原先的 HTTP + SSE 作为默认传输方式。这一变更在解决原有方案中连接不可恢复、服务端长连接压力大等问题的同时,依然保留了 SSE 带来的流式响应优势。
Streamable HTTP 并不是传统意义上的 流式 HTTP(Streaming HTTP),它指的是一种 兼具以下特性的传输机制:
以普通 HTTP
请求为基础,客户端用 POST/GET
发请求;
服务器可选地将响应升级为 SSE
流,实现 流式传输 的能力(当需要时);
去中心化、无强制要求持续连接,支持 stateless
模式;
客户端和服务端之间的消息传输更加灵活,比如同一个 /message
端点可用于发起请求和接收 SSE
流;
不再需要单独的 /sse
端点,一切通过统一的 /message
协议层处理。
Streamable HTTP优势:
支持无状态服务器实现
向后兼容现有HTTP+SSE方案
灵活的传输方式选择
更好的基础设施集成
原有问题:
SSE连接断线无法恢复
服务器需维持高可用长连接
单向被动响应,无法主动推送
传统的Function Calling存在以下核心问题:
// 传统Function Calling - 工具必须硬编码在应用中
func handleFunctionCall(name string, args map[string]interface{}) error {
switch name {
case "calculator":
return handleCalculator(args)
case "weather":
return handleWeather(args)
case "database_query":
return handleDatabaseQuery(args)
// 每添加新工具都需要修改这里
default:
return fmt.Errorf("unknown function: %s", name)
}
}
// Function Calling - 每次调用都是独立的
func calculateSum(numbers [ ]float64) float64 {
// 无法记住之前的计算历史
// 无法维护计算上下文
return sum(numbers)
}
添加新工具需要重新编译整个应用
工具错误可能导致整个应用崩溃
资源隔离困难,内存泄漏影响全局
特性维度 | Function Calling | MCP |
---|---|---|
部署方式 | 嵌入式,需重新编译 | 独立服务,热插拔 |
错误隔离 | 共享进程,相互影响 | 进程隔离,独立恢复 |
资源管理 | 共享内存,难以控制 | 独立资源配额 |
扩展性 | 线性增长复杂度 | 水平扩展,负载均衡 |
语言限制 | 单一语言栈 | 跨语言生态 |
调试难度 | 复杂,影响主程序 | 独立调试,不影响其他组件 |
版本管理 | 强耦合版本依赖 | 独立版本控制 |
可用的go-mcp库:
https://github.com/mark3labs/mcp-go
https://github.com/modelcontextprotocol/go-sdk
// go.mod
module golang-mcp-server
go 1.21
require (
github.com/mark3labs/mcp-go v0.36.0
)
require (
github.com/google/uuid v1.6.0 // UUID生成
github.com/spf13/cast v1.7.1 // 类型转换
gopkg.in/yaml.v3 v3.0.1 // 配置文件解析
)
package main
import (
"context"
"fmt"
"log"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func main() {
// 创建MCP服务器实例
s := server.NewMCPServer(
"go-mcp-server", // 服务器名称
"1.0.0", // 版本号
server.WithToolCapabilities(true), // 启用工具功能
server.WithRecovery(), // 启用错误恢复
)
// 定义问候工具
helloTool := mcp.NewTool("say_hello",
mcp.WithDescription("向指定的人问好"),
mcp.WithString("name",
mcp.Required(),
mcp.Description("要问候的人的姓名"),
),
mcp.WithString("language",
mcp.Description("问候语言:zh, en, ja"),
),
)
// 注册工具处理函数
s.AddTool(helloTool, handleHello)
// 启动服务器
if err := server.ServeStdio(s); err != nil {
log.Fatalf("服务器启动失败: %v", err)
}
}
// 问候工具处理函数
func handleHello(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 提取必需参数
name, err := request.RequireString("name")
if err != nil {
return mcp.NewToolResultError("缺少name参数"), nil
}
// 提取可选参数
language := request.GetString("language", "zh")
// 根据语言生成问候语
var greeting string
switch language {
case "en":
greeting = fmt.Sprintf("Hello, %s! Welcome to MCP!", name)
case "ja":
greeting = fmt.Sprintf("こんにちは、%sさん!MCPへようこそ!", name)
default:
greeting = fmt.Sprintf("你好, %s! 欢迎使用MCP!", name)
}
return mcp.NewToolResultText(greeting), nil
}
基于仓库中的实际代码,扩展计算器功能:
package main
import (
"context"
"fmt"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"math"
)
func main() {
// 创建MCP服务器实例
s := server.NewMCPServer(
"go-mcp-server", // 服务器名称
"1.0.0", // 版本号
server.WithToolCapabilities(true), // 启用工具功能
server.WithRecovery(), // 启用错误恢复
)
// 基础四则运算
addTool := mcp.NewTool("add",
mcp.WithDescription("执行加法运算"),
mcp.WithNumber("a", mcp.Required(), mcp.Description("第一个数字")),
mcp.WithNumber("b", mcp.Required(), mcp.Description("第二个数字")),
)
subtractTool := mcp.NewTool("subtract",
mcp.WithDescription("执行减法运算"),
mcp.WithNumber("a", mcp.Required(), mcp.Description("被减数")),
mcp.WithNumber("b", mcp.Required(), mcp.Description("减数")),
)
multiplyTool := mcp.NewTool("multiply",
mcp.WithDescription("执行乘法运算"),
mcp.WithNumber("a", mcp.Required(), mcp.Description("第一个因数")),
mcp.WithNumber("b", mcp.Required(), mcp.Description("第二个因数")),
)
divideTool := mcp.NewTool("divide",
mcp.WithDescription("执行除法运算"),
mcp.WithNumber("a", mcp.Required(), mcp.Description("被除数")),
mcp.WithNumber("b", mcp.Required(), mcp.Description("除数")),
)
// 高级数学函数
powerTool := mcp.NewTool("power",
mcp.WithDescription("计算幂运算"),
mcp.WithNumber("base", mcp.Required(), mcp.Description("底数")),
mcp.WithNumber("exponent", mcp.Required(), mcp.Description("指数")),
)
sqrtTool := mcp.NewTool("sqrt",
mcp.WithDescription("计算平方根"),
mcp.WithNumber("number", mcp.Required(), mcp.Description("要计算平方根的数")),
)
// 注册所有工具
s.AddTool(addTool, handleAdd)
s.AddTool(subtractTool, handleSubtract)
s.AddTool(multiplyTool, handleMultiply)
s.AddTool(divideTool, handleDivide)
s.AddTool(powerTool, handlePower)
s.AddTool(sqrtTool, handleSqrt)
if err := server.ServeStdio(s); err != nil {
fmt.Printf("Server error: %v\n", err)
}
}
// 基础运算处理函数
func handleAdd(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
a, err := request.RequireFloat("a")
if err != nil {
return mcp.NewToolResultError("参数a必须是数字"), nil
}
b, err := request.RequireFloat("b")
if err != nil {
return mcp.NewToolResultError("参数b必须是数字"), nil
}
result := a + b
return mcp.NewToolResultText(fmt.Sprintf("%.2f + %.2f = %.2f", a, b, result)), nil
}
func handleSubtract(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
a, err := request.RequireFloat("a")
if err != nil {
return mcp.NewToolResultError("参数a必须是数字"), nil
}
b, err := request.RequireFloat("b")
if err != nil {
return mcp.NewToolResultError("参数b必须是数字"), nil
}
result := a - b
return mcp.NewToolResultText(fmt.Sprintf("%.2f - %.2f = %.2f", a, b, result)), nil
}
func handleMultiply(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
a, err := request.RequireFloat("a")
if err != nil {
return mcp.NewToolResultError("参数a必须是数字"), nil
}
b, err := request.RequireFloat("b")
if err != nil {
return mcp.NewToolResultError("参数b必须是数字"), nil
}
result := a * b
return mcp.NewToolResultText(fmt.Sprintf("%.2f × %.2f = %.2f", a, b, result)), nil
}
func handleDivide(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
a, err := request.RequireFloat("a")
if err != nil {
return mcp.NewToolResultError("参数a必须是数字"), nil
}
b, err := request.RequireFloat("b")
if err != nil {
return mcp.NewToolResultError("参数b必须是数字"), nil
}
if b == 0 {
return mcp.NewToolResultError("除数不能为零"), nil
}
result := a / b
return mcp.NewToolResultText(fmt.Sprintf("%.2f ÷ %.2f = %.2f", a, b, result)), nil
}
// 高级运算处理函数
func handlePower(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
base, err := request.RequireFloat("base")
if err != nil {
return mcp.NewToolResultError("底数必须是数字"), nil
}
exponent, err := request.RequireFloat("exponent")
if err != nil {
return mcp.NewToolResultError("指数必须是数字"), nil
}
result := math.Pow(base, exponent)
return mcp.NewToolResultText(fmt.Sprintf("%.2f ^ %.2f = %.2f", base, exponent, result)), nil
}
func handleSqrt(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
number, err := request.RequireFloat("number")
if err != nil {
return mcp.NewToolResultError("参数必须是数字"), nil
}
if number < 0 {
return mcp.NewToolResultError("不能计算负数的平方根"), nil
}
result := math.Sqrt(number)
return mcp.NewToolResultText(fmt.Sprintf("√%.2f = %.2f", number, result)), nil
}
基于仓库中现有的搜索功能,集成到MCP服务器:
func main() {
// 创建 MCP Server 实例
s := server.NewMCPServer(
"Google Search MCP Server",
"1.0.0",
)
// 定义工具元数据
googleSearchTool := mcp.NewTool(
"google_search",
mcp.WithDescription("使用 Google(Serper) 搜索查询信息,联网搜索、查询、获取信息"),
mcp.WithString("query",
mcp.Required(),
mcp.Description("要搜索的查询内容"),
),
)
// 注册工具及其处理函数
s.AddTool(googleSearchTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 解析参数
type searchParams struct {
Query string `json:"query"`
Num int `json:"num"`
}
var params searchParams
paramsJSON, err := json.Marshal(request.Params.Arguments)
if err != nil {
return nil, fmt.Errorf("failed to marshal params: %w", err)
}
if err := json.Unmarshal(paramsJSON, ¶ms); err != nil {
return nil, fmt.Errorf("failed to unmarshal params: %w", err)
}
if params.Query == "" {
return nil, fmt.Errorf("query parameter is required")
}
// 调用搜索逻辑
searchResp, err := search.GoogleSearch(params.Query)
if err != nil {
return nil, fmt.Errorf("search failed: %w", err)
}
// 格式化响应,只保留答案框和前 5 条有机搜索结果
var results []map[string]interface{}
if searchResp.AnswerBox.Answer != "" {
results = append(results, map[string]interface{}{
"type": "answer",
"answer": searchResp.AnswerBox.Answer,
})
}
for i, organic := range searchResp.Organic {
if i >= 5 {
break
}
results = append(results, map[string]interface{}{
"type": "organic",
"title": organic.Title,
"link": organic.Link,
"snippet": organic.Snippet,
})
}
resultJSON, err := json.Marshal(map[string]interface{}{
"query": params.Query,
"results": results,
})
if err != nil {
return nil, fmt.Errorf("failed to marshal results: %w", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(string(resultJSON)),
},
}, nil
})
// -------- HTTP & SSE 服务 --------
addr := ":8080"
mux := http.NewServeMux()
mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
query := r.URL.Query().Get("query")
log.Printf("[HTTP] /search query=%s from %s", query, r.RemoteAddr)
if query == "" {
http.Error(w, "query parameter is required", http.StatusBadRequest)
return
}
results, err := doSearch(query)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(map[string]any{
"query": query,
"results": results,
}); err != nil {
log.Printf("encode error: %v", err)
} else {
log.Printf("[HTTP] /search query=%s completed in %v", query, time.Since(start))
}
})
// SSE 端点
mux.HandleFunc("/sse", func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[SSE] connection from %s", r.RemoteAddr)
f, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
query := r.URL.Query().Get("query")
if query == "" {
fmt.Fprintf(w, "data: %s\n\n", "{\"error\":\"query parameter is required\"}")
f.Flush()
return
}
results, err := doSearch(query)
if err != nil {
fmt.Fprintf(w, "data: %s\n\n", fmt.Sprintf("{\"error\":\"%s\"}", err.Error()))
f.Flush()
return
}
for _, item := range results {
b, _ := json.Marshal(item)
fmt.Fprintf(w, "data: %s\n\n", b)
f.Flush()
}
log.Printf("[SSE] query=%s completed in %v", query, time.Since(start))
})
// 启动 stdio MCP 服务器
go func() {
if err := server.ServeStdio(s); err != nil {
log.Printf("MCP stdio server error: %v", err)
}
}()
// 构造 HTTP 服务器
httpSrv := &http.Server{
Addr: addr,
Handler: mux,
}
go func() {
log.Printf("[GoogleSearch HTTP] 监听 %s", addr)
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("HTTP server error: %v", err)
}
}()
// -------- 退出 --------
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
ctxShutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := httpSrv.Shutdown(ctxShutdown); err != nil {
log.Printf("HTTP shutdown error: %v", err)
}
}
将所有功能整合到一个完整的服务器中:
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"golang-mcp/search"
"log"
"os"
"os/signal"
"syscall"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func main() {
// 聚合服务器
s := server.NewMCPServer(
"go-mcp-server",
"1.0.0",
server.WithResourceCapabilities(true, true), //启用资源功能
server.WithToolCapabilities(true), //启用工具功能
)
// 注册tools 资源 prompt
registerCalculateTool(s)
registerSearchTool(s)
registerGreetingPrompt(s)
registerResources(s)
// ----------- 启动MCP SSE服务器 ----------
sseServer := server.NewSSEServer(s)
go func() {
log.Println("启动MCP SSE服务器在端口 :8080")
if err := sseServer.Start(":8080"); err != nil {
log.Printf("MCP SSE server error: %v", err)
}
}()
// ----------- 启动Streamable HTTP服务器 ----------
go func() {
streamableServer := server.NewStreamableHTTPServer(s)
log.Println("启动MCP Streamable HTTP服务器在端口 :8081")
if err := streamableServer.Start(":8081"); err != nil {
log.Printf("Streamable HTTP server error: %v", err)
}
}()
// ----------- 启动 stdio ----------
go func() {
if err := server.ServeStdio(s); err != nil {
log.Printf("stdio server error: %v", err)
}
}()
// ----------- 处理退出 ----------
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
log.Println("MCP Server is running. Press Ctrl+C to stop.")
log.Printf("可用传输模式:")
log.Printf(" SSE: http://localhost:8080/sse (兼容模式)")
log.Printf(" Streamable HTTP: http://localhost:8081/mcp (推荐)")
log.Printf(" stdio: 标准输入输出")
log.Printf("----可用功能:-----")
log.Printf(" 工具: calculate, google_search")
log.Printf(" 资源: docs://readme")
log.Printf(" 提示: greeting")
<-quit
log.Println("shutting down…")
}
// 公共搜索封装
func doSearch(query string) ([]map[string]any, error) {
resp, err := search.GoogleSearch(query)
if err != nil {
return nil, err
}
var results []map[string]any
if resp.AnswerBox.Answer != "" {
results = append(results, map[string]any{"type": "answer", "answer": resp.AnswerBox.Answer})
}
for i, o := range resp.Organic {
if i >= 5 {
break
}
results = append(results, map[string]any{"type": "organic", "title": o.Title, "link": o.Link, "snippet": o.Snippet})
}
return results, nil
}
// registerGreetingPrompt 提供简单问候 prompt
func registerGreetingPrompt(s *server.MCPServer) {
s.AddPrompt(mcp.NewPrompt("greeting",
mcp.WithPromptDescription("一个友好的问候提示"),
mcp.WithArgument("name",
mcp.ArgumentDescription("要问候的人的名字"),
),
mcp.WithArgument("language",
mcp.ArgumentDescription("问候语言:zh, en, ja"),
),
), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
type greetingParams struct {
Name string `json:"name"`
Language string `json:"language"`
}
var params greetingParams
paramsJSON, err := json.Marshal(request.Params.Arguments)
if err != nil {
return nil, fmt.Errorf("failed to marshal params: %w", err)
}
if err := json.Unmarshal(paramsJSON, ¶ms); err != nil {
return nil, fmt.Errorf("failed to unmarshal params: %w", err)
}
name := params.Name
if name == "" {
name = "朋友"
}
// 提取可选参数
language := params.Name
if language == "" {
language = "zh"
}
// 根据语言生成问候语
var greeting string
switch language {
case "en":
greeting = fmt.Sprintf("Hello, %s! Welcome to MCP!", name)
case "ja":
greeting = fmt.Sprintf("こんにちは、%sさん!MCPへようこそ!", name)
default:
greeting = fmt.Sprintf("你好, %s! 欢迎使用MCP!", name)
}
return mcp.NewGetPromptResult(
"友好的问候",
[]mcp.PromptMessage{
mcp.NewPromptMessage(
mcp.RoleAssistant,
mcp.NewTextContent(greeting),
),
},
), nil
})
}
func registerResources(s *server.MCPServer) {
// 静态资源示例 - 暴露一个 README 文件
resource := mcp.NewResource(
"docs://readme",
"项目说明文档",
mcp.WithResourceDescription("项目的 README 文件"),
mcp.WithMIMEType("text/markdown"),
)
// 添加资源及其处理函数
s.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
content, err := os.ReadFile("README.md")
if err != nil {
return nil, err
}
return []mcp.ResourceContents{
mcp.TextResourceContents{
URI: "docs://readme",
MIMEType: "text/markdown",
Text: string(content),
},
}, nil
})
}
// registerCalculateTool 将简单的四则运算工具添加到 server
func registerCalculateTool(s *server.MCPServer) {
calcTool := mcp.NewTool("calculate",
mcp.WithDescription("执行基本的算术运算"),
mcp.WithString("operation", mcp.Required(), mcp.Description("运算类型: add/subtract/multiply/divide"), mcp.Enum("add", "subtract", "multiply", "divide")),
mcp.WithNumber("x", mcp.Required(), mcp.Description("第一个数字")),
mcp.WithNumber("y", mcp.Required(), mcp.Description("第二个数字")),
)
s.AddTool(calcTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
var params struct {
Operation string `json:"operation"`
X float64 `json:"x"`
Y float64 `json:"y"`
}
buf, _ := json.Marshal(req.Params.Arguments)
_ = json.Unmarshal(buf, ¶ms)
var result float64
switch params.Operation {
case "add":
result = params.X + params.Y
case "subtract":
result = params.X - params.Y
case "multiply":
result = params.X * params.Y
case "divide":
if params.Y == 0 {
return nil, errors.New("不允许除以零")
}
result = params.X / params.Y
default:
return nil, fmt.Errorf("未知运算: %s", params.Operation)
}
return mcp.FormatNumberResult(result), nil
})
}
// registerSearchTool 使用之前封装的 search.GoogleSearch
func registerSearchTool(s *server.MCPServer) {
searchTool := mcp.NewTool("google_search",
mcp.WithDescription("使用 Google(Serper) 搜索查询信息,联网搜索、查询、获取信息"),
mcp.WithString("query", mcp.Required(), mcp.Description("要搜索的查询内容")),
)
s.AddTool(searchTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
var p struct {
Query string `json:"query"`
}
b, _ := json.Marshal(req.Params.Arguments)
_ = json.Unmarshal(b, &p)
if p.Query == "" {
return nil, fmt.Errorf("query 不能为空")
}
results, err := doSearch(p.Query)
if err != nil {
return nil, err
}
out, _ := json.Marshal(map[string]any{"query": p.Query, "results": results})
return &mcp.CallToolResult{Content: []mcp.Content{mcp.NewTextContent(string(out))}}, nil
})
}
CURL调用
POST http://localhost:8081/message
{
"method":"tools/call",
"params":{
"name":"google_search",
"arguments":{
"query": "golang教程"
}
}
}
Response
{
"id": null,
"jsonrpc": "2.0",
"result": {
"content": [
{
"text": "{\"query\":\"golang教程\",\"results\":[{\"link\":\"http://www.runoob.com/go/go-tutorial.html\",\"snippet\":\"Go 语言教程Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。 Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发, ...\",\"title\":\"Go 语言教程\",\"type\":\"organic\"},{\"link\":\"https://tour.go-zh.org/\",\"snippet\":\"欢迎访问Go 编程语言教程。 本教程分为几个模块,点击本页左上角的Go 语言之旅可以查看模块列表。 你也可以随时点击页面右上角的菜单来浏览目录。 本教程由一系列幻灯 ...\",\"title\":\"Go 语言之旅\",\"type\":\"organic\"},{\"link\":\"https://www.topgoer.com/\",\"snippet\":\"前景 · 开发环境 · Go的安装 · 配置GOPATH · 编辑器 · Git安装 · 第一个go程序 · Go基础 · Go语言的主要特征 · Golang内置类型和函数 · Init函数和main函数 · 命令 ...\",\"title\":\"前景· Go语言中文文档\",\"type\":\"organic\"}]}",
"type": "text"
}
]
}
}
# 设置环境变量
export SERPER_API_KEY="your_serper_api_key"
# 构建项目
go build -o mcp-server main.go
# 运行服务器
./mcp-server
# 或者直接运行
go run main.go
可以使用 mcp-go
提供的 client
模块,构建一个通过 stdio
方式连接到前面打包好的 MCP
服务器的客户端。
初始化客户端并连接服务器
获取提示词、资源、工具列表
调用远程工具(tool)
client.go
package main
import (
"context"
"encoding/base64"
"fmt"
"os"
"strings"
"time"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
)
func main() {
// 创建一个基于 stdio 的MCP客户端
mcpClient, err := client.NewStdioMCPClient(
"D:/mcp-golang/go-mcp-servers.exe",
[]string{},
)
if err != nil {
panic(err)
}
defer mcpClient.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
fmt.Println("初始化 mcp 客户端...")
initRequest := mcp.InitializeRequest{}
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
Name: "Client Demo",
Version: "1.0.0",
}
// 初始化MCP客户端并连接到服务器
initResult, err := mcpClient.Initialize(ctx, initRequest)
if err != nil {
panic(err)
}
fmt.Printf(
"\n初始化成功,服务器信息: %s %s\n\n",
initResult.ServerInfo.Name,
initResult.ServerInfo.Version,
)
// 从服务器获取提示词列表
fmt.Println("提示词列表:")
promptsRequest := mcp.ListPromptsRequest{}
prompts, err := mcpClient.ListPrompts(ctx, promptsRequest)
if err != nil {
panic(err)
}
for _, prompt := range prompts.Prompts {
fmt.Printf("- %s: %s\n", prompt.Name, prompt.Description)
fmt.Println("参数:", prompt.Arguments)
}
// 从服务器获取资源列表
fmt.Println("资源列表:")
resourcesRequest := mcp.ListResourcesRequest{}
resources, err := mcpClient.ListResources(ctx, resourcesRequest)
fmt.Println(resources)
fmt.Println(err)
if err != nil {
panic(err)
}
// 遍历资源列表并逐个读取内容
for _, r := range resources.Resources {
fmt.Printf("\n--- 读取资源 %s ---\n", r.URI)
// 1. 构造 ReadResource 请求
readReq := mcp.ReadResourceRequest{
Request: mcp.Request{
Method: "resources/read",
},
}
readReq.Params.URI = r.URI // 指定要读取的 URI
// 2. 调用客户端方法
readRes, err := mcpClient.ReadResource(ctx, readReq)
if err != nil {
fmt.Printf("读取 %s 失败: %v\n", r.URI, err)
continue
}
// 3. 处理返回的 contents
for _, c := range readRes.Contents {
switch v := c.(type) {
// ---------- 纯文本 ----------
case mcp.TextResourceContents:
fmt.Printf("【文本内容】(%s)\n", v.MIMEType)
fmt.Println(v.Text)
// ---------- 二进制(Blob)----------
case mcp.BlobResourceContents:
// v.Blob 是 base64 字符串
raw, err := base64.StdEncoding.DecodeString(v.Blob)
if err != nil {
fmt.Println("base64 解码失败:", err)
continue
}
fileName := strings.ReplaceAll(r.URI, "://", "_")
if err := os.WriteFile(fileName, raw, 0644); err != nil {
fmt.Printf("保存 %s 失败: %v\n", fileName, err)
} else {
fmt.Printf("二进制内容已写入本地文件: %s (%d bytes)\n", fileName, len(raw))
}
}
}
}
// 从服务器获取工具列表
fmt.Println("可用工具列表:")
toolsRequest := mcp.ListToolsRequest{}
tools, err := mcpClient.ListTools(ctx, toolsRequest)
if err != nil {
panic(err)
}
for _, tool := range tools.Tools {
fmt.Printf("- %s: %s\n", tool.Name, tool.Description)
fmt.Println("参数:", tool.InputSchema.Properties)
}
// 调用计算工具
fmt.Println("调用工具: calculate")
toolRequest := mcp.CallToolRequest{
Request: mcp.Request{
Method: "tools/call",
},
}
toolRequest.Params.Name = "calculate"
toolRequest.Params.Arguments = map[string]any{
"operation": "add",
"x": 1,
"y": 1,
}
result, err := mcpClient.CallTool(ctx, toolRequest)
if err != nil {
panic(err)
}
fmt.Println("调用工具结果:", result.Content[0].(mcp.TextContent).Text)
// 调用搜索工具
fmt.Println("调用google_search工具: google_search")
toolRequest = mcp.CallToolRequest{
Request: mcp.Request{
Method: "tools/call",
},
}
toolRequest.Params.Name = "google_search"
toolRequest.Params.Arguments = map[string]any{
"query": "golang教程",
}
result, err = mcpClient.CallTool(ctx, toolRequest)
if err != nil {
panic(err)
}
fmt.Println("调用google_search工具:", result.Content[0].(mcp.TextContent).Text)
}
创建一个简单的测试客户端:
package main
import (
"bufio"
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"time"
)
const (
baseURL = "http://localhost:8080"
)
// 测试HTTP搜索接口
func TestHTTPSearch(t *testing.T) {
tests := []struct {
name string
query string
expectError bool
}{
{"正常查询", "Go语言教程", false},
{"英文查询", "golang programming", false},
{"空查询", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
url := fmt.Sprintf("%s/search", baseURL)
if tt.query != "" {
url += "?query=" + tt.query
}
resp, err := http.Get(url)
if err != nil {
t.Fatalf("请求失败: %v (请确保main程序正在运行)", err)
}
defer resp.Body.Close()
if tt.expectError {
if resp.StatusCode == http.StatusOK {
t.Error("期望请求失败,但请求成功了")
}
return
}
if resp.StatusCode != http.StatusOK {
t.Errorf("期望状态码 200, 得到 %d", resp.StatusCode)
return
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Errorf("响应解析失败: %v", err)
return
}
if result["query"] != tt.query {
t.Errorf("查询参数不匹配,期望: %s, 得到: %s", tt.query, result["query"])
}
if results, ok := result["results"].([]interface{}); ok {
t.Logf("✅ HTTP搜索成功,查询: '%s', 返回 %d 个结果", tt.query, len(results))
// 显示前2个结果
for i, item := range results {
if i >= 2 {
break
}
if itemMap, ok := item.(map[string]interface{}); ok {
if title, ok := itemMap["title"].(string); ok {
t.Logf(" 结果 %d: %s", i+1, title)
}
}
}
} else {
t.Error("结果格式不正确")
}
})
}
}
// 测试SSE搜索接口
func TestSSESearch(t *testing.T) {
tests := []struct {
name string
query string
expectError bool
}{
{"正常SSE查询", "Python教程", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
url := fmt.Sprintf("%s/sse", baseURL)
if tt.query != "" {
url += "?query=" + tt.query
}
resp, err := http.Get(url)
if err != nil {
t.Fatalf("SSE请求失败: %v (请确保main程序正在运行)", err)
}
defer resp.Body.Close()
// 检查SSE头部
contentType := resp.Header.Get("Content-Type")
if contentType != "text/event-stream" {
t.Errorf("期望Content-Type为text/event-stream, 得到 %s", contentType)
}
// 读取SSE流
scanner := bufio.NewScanner(resp.Body)
eventCount := 0
hasError := false
// 设置超时,避免长时间等待
timeout := time.After(10 * time.Second)
done := make(chan bool)
go func() {
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "data: ") {
eventCount++
data := strings.TrimPrefix(line, "data: ")
if strings.Contains(data, "error") {
hasError = true
t.Logf("SSE错误事件: %s", data)
break
}
// 尝试解析数据
var item map[string]interface{}
if err := json.Unmarshal([]byte(data), &item); err == nil {
if title, ok := item["title"].(string); ok {
t.Logf("SSE数据: %s", title)
}
}
if eventCount >= 3 { // 限制读取事件数量
break
}
}
}
done <- true
}()
select {
case <-timeout:
t.Log("SSE读取超时")
case <-done:
break
}
if tt.expectError {
if !hasError && eventCount == 0 {
t.Error("期望有错误或没有数据")
}
} else {
if hasError {
t.Error("不期望有错误")
} else {
t.Logf("✅ SSE搜索成功,查询: '%s', 接收到 %d 个事件", tt.query, eventCount)
}
}
})
}
}
// 简单的端点可用性测试
func TestServerAvailability(t *testing.T) {
resp, err := http.Get(fmt.Sprintf("%s/search?query=test", baseURL))
if err != nil {
t.Fatalf("服务器不可用: %v (请确保运行 go run main.go)", err)
}
defer resp.Body.Close()
t.Log("✅ 服务器运行正常")
}
// 并发测试
func TestConcurrentRequests(t *testing.T) {
queries := []string{"Go", "Python", "Java", "JavaScript", "Rust"}
results := make(chan error, len(queries))
// 并发发送HTTP请求
for _, query := range queries {
go func(q string) {
url := fmt.Sprintf("%s/search?query=%s", baseURL, q)
resp, err := http.Get(url)
if err != nil {
results <- err
return
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
results <- fmt.Errorf("状态码: %d", resp.StatusCode)
return
}
results <- nil
}(query)
}
// 收集结果
successCount := 0
for i := 0; i < len(queries); i++ {
if err := <-results; err != nil {
t.Logf("并发请求失败: %v", err)
} else {
successCount++
}
}
if successCount == 0 {
t.Error("所有并发请求都失败了")
} else {
t.Logf("✅ 并发测试完成,%d/%d 成功", successCount, len(queries))
}
}
// 性能基准测试
func BenchmarkHTTPSearch(b *testing.B) {
url := fmt.Sprintf("%s/search?query=benchmark", baseURL)
b.ResetTimer()
for i := 0; i < b.N; i++ {
resp, err := http.Get(url)
if err != nil {
b.Fatalf("基准测试请求失败: %v", err)
}
resp.Body.Close()
}
}
mcp.json配置:
"go-mcp-servers": {
"command": "D:\mcp-golang\go-mcp-servers.exe",
"args": [],
"env": {
"GOPATH": "D:\Program Files\GoPath\go1.24.5",
"GOMODCACHE": "D:\Program Files\GoPath\pkg\md"
}
}
Prompt触发调用:
npx @modelcontextprotocol/inspector
Streamable HTTP:http://localhost:8081/mcp
如果您喜欢我的文章,请点击下面按钮随意打赏,您的支持是我最大的动力。
最新评论