据说Go官方也在更加拥抱MCP,也就是AI。前两天也做了一点测试。
MCP 是 API 的一个子集,专为 AI 模型交互优化,更关注 高效计算、低延迟、大规模模型数据交换。
MCP与API快速对比
功能 | MCP | 传统API |
---|---|---|
整合难度 | 一次标准化整合 | 每个API单独整合 |
实时双向通信 | ✅ 支持 | ❌ 不支持 |
动态发现工具 | ✅ 支持 | ❌ 不支持 |
扩展性 | 即插即用 | 需要额外开发 |
安全性与控制 | 所有工具统一标准 | 每个API单独定义 |
MCP通过公开工具的接口信息,让AI(或调用者)来自动发现API服务商的要求和变更。
至于动态发现,现在还没看到。我所理解的动态发现是指(例如本地Stdio),无需指定而能够自动找到服务工具。测试了VSCode中的Cline,还是需要指定MCP服务的。
对于Http的服务,要实现自动发现,估计得做一个MCP"服务器的服务器",把MCP服务挂接到这样的综合服务端。
1 Go-MCP
Go-MCP 是一个功能强大且易于使用的 Go 客户端库,专为与MCP进行交互而设计.
它的官方示例只使用了SSE方式,即网络通信模式。MCP还支持Stdio(标准输入/输出)的通信方式,适合于本地进程间通信。
介绍文章可以看看。
服务端
package main
import (
"encoding/json"
"fmt"
"log"
"time"
"github.com/ThinkInAIXYZ/go-mcp/protocol"
"github.com/ThinkInAIXYZ/go-mcp/server"
"github.com/ThinkInAIXYZ/go-mcp/transport"
)
type currentTimeReq struct {
Timezone string `json:"timezone" description:"current time timezone"`
}
func main() {
// 创建传输服务器(本例使用 SSE)
transportServer, err := transport.NewSSEServerTransport("127.0.0.1:8080")
if err != nil {
log.Fatalf("Failed to create transport server: %v", err)
}
// 使用传输创建 MCP 服务器
mcpServer, err := server.NewServer(transportServer,
// 设置服务器实现信息
server.WithServerInfo(protocol.Implementation{
Name: "示例 MCP 服务器",
Version: "1.0.0",
}),
)
if err != nil {
log.Fatalf("Failed to create MCP server: %v", err)
}
tool, err := protocol.NewTool("current time", "Get current time with timezone, Asia/Shanghai is default", currentTimeReq{})
if err != nil {
log.Fatalf("Failed to create tool: %v", err)
return
}
// 注册工具处理器
mcpServer.RegisterTool(tool, func(request *protocol.CallToolRequest) (*protocol.CallToolResult, error) {
req := new(currentTimeReq)
if err := json.Unmarshal(request.RawArguments, &req); err != nil {
return nil, err
}
loc, err := time.LoadLocation(req.Timezone)
if err != nil {
return nil, fmt.Errorf("parse timezone with error: %v", err)
}
text := fmt.Sprintf(`current time is %s`, time.Now().In(loc))
return &protocol.CallToolResult{
Content: []protocol.Content{
protocol.TextContent{
Type: "text",
Text: text,
},
},
}, nil
})
if err = mcpServer.Run(); err != nil {
log.Fatalf("Failed to start MCP server: %v", err)
return
}
}
客户端
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/ThinkInAIXYZ/go-mcp/client"
"github.com/ThinkInAIXYZ/go-mcp/protocol"
"github.com/ThinkInAIXYZ/go-mcp/transport"
)
func main() {
// Create transport client (using SSE in this example)
transportClient, err := transport.NewSSEClientTransport("http://127.0.0.1:8080/sse")
if err != nil {
log.Fatalf("Failed to create transport client: %v", err)
}
// Create MCP client using transport
mcpClient, err := client.NewClient(transportClient, client.WithClientInfo(protocol.Implementation{
Name: "示例 MCP 客户端",
Version: "1.0.0",
}))
if err != nil {
log.Fatalf("Failed to create MCP client: %v", err)
}
defer mcpClient.Close()
// List available tools
toolsResult, err := mcpClient.ListTools(context.Background())
if err != nil {
log.Fatalf("Failed to list tools: %v", err)
}
b, _ := json.Marshal(toolsResult.Tools)
fmt.Printf("Available tools: %+v\n", string(b))
// Call tool
callResult, err := mcpClient.CallTool(
context.Background(),
protocol.NewCallToolRequest("current time", map[string]interface{}{
"timezone": "UTC",
}))
if err != nil {
log.Fatalf("Failed to call tool: %v", err)
}
b, _ = json.Marshal(callResult)
fmt.Printf("Tool call result: %+v\n", string(b))
}
命令行信息显示:
可用的工具: [{"description":"获取带时区的时间, 默认为Asia/Shanghai","inputSchema":{"type":"object","properties":{"timezone":{"type":"string","description":"当前时区"}},"required":["timezone"]},"name":"current time"}]
工具返回: {"content":[{"type":"text","text":"current time is 2025-04-10 02:32:54.1009555 +0000 UTC"}]}
浏览器
既然是Web服务,通过浏览器访问显示如下内容,且一直在转圈,似乎等待交互的样子(还没有读MCP的SSE逻辑)。 Firefox浏览器显示一片空白,且一直转圈
event: endpoint
data: http://127.0.0.1:8080/message?sessionID=d2c941ac-45db-4a91-bbcd-8ff3c9dfae70
与其它Http服务整合
既然SSE是Http服务,也可以与其它Web服务整合到一起.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"github.com/ThinkInAIXYZ/go-mcp/protocol"
"github.com/ThinkInAIXYZ/go-mcp/server"
"github.com/ThinkInAIXYZ/go-mcp/transport"
)
type currentTimeReq struct {
Timezone string `json:"timezone" description:"当前时区"`
}
func main() {
var (
messageUrl = "/message"
port = "8080"
)
// 创建传输服务器 (SSE)
transport, handler, err := transport.NewSSEServerTransportAndHandler(fmt.Sprintf("http://127.0.0.1:%s%s", port, messageUrl))
if err != nil {
log.Fatalf("创建SSE传输失败: %v", err)
}
mcpServer, err := server.NewServer(transport,
server.WithServerInfo(protocol.Implementation{
Name: "示例 MCP 服务器",
Version: "1.0.0",
}),
)
if err != nil {
log.Fatalf("创建MCP服务失败: %v", err)
}
// 注册工具句柄
tool, err := protocol.NewTool("current time", "获取带时区的时间, 默认为Asia/Shanghai", currentTimeReq{})
if err != nil {
log.Fatalf("创建工具失败: %v", err)
return
}
// 新的工具句柄和返回值
toolHandler := func(request *protocol.CallToolRequest) (*protocol.CallToolResult, error) {
req := new(currentTimeReq)
if err := json.Unmarshal(request.RawArguments, &req); err != nil {
return nil, err
}
loc, err := time.LoadLocation(req.Timezone)
if err != nil {
return nil, fmt.Errorf("parse timezone with error: %v", err)
}
text := fmt.Sprintf(`current time is %s`, time.Now().In(loc))
return &protocol.CallToolResult{
Content: []protocol.Content{
protocol.TextContent{
Type: "text",
Text: text,
},
},
}, nil
}
mcpServer.RegisterTool(tool, toolHandler)
// 设置 HTTP 路由
http.Handle("/sse", handler.HandleSSE())
http.Handle(messageUrl, handler.HandleMessage())
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte("<h1>欢迎使用MCP服务器</h1><p>请访问 <a href='/sse'>/sse</a> 来查看SSE消息。</p>")) // 返回简单的HTML页面
}))
// 启动 HTTP 服务器
fmt.Println("启动MCP服务器 :8080...")
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatalf("启动HTTP服务失败: %v", err)
}
}
客户端调用了没有?不太清楚。添加一个中间件,看看哪些“人”来过。
...
// 中间件函数
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("收到请求: %s %s %s", r.RemoteAddr, r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
...
http.Handle("/sse", middleware(handler.HandleSSE()))
http.Handle(messageUrl, middleware(handler.HandleMessage()))
http.Handle("/", middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte("<h1>欢迎使用MCP服务器</h1><p>请访问 <a href='/sse'>/sse</a> 来查看SSE消息。</p>")) // 返回简单的HTML页面
})))
2 mcp-go
这是另一个实现的MCP协议
服务端
示例里支持了两种通信方式
package main
import (
"context"
"errors"
"fmt"
"os"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func main() {
// 创建MCP服务器
s := server.NewMCPServer(
"我的计算器",
"1.0.0",
server.WithResourceCapabilities(true, true), // 启用资源能力
server.WithLogging(), // 启用日志记录
)
// 添加工具
{
calculatorTool := mcp.NewTool("calculate",
mcp.WithDescription("执行基本的算术运算"),
mcp.WithString("operation",
mcp.Required(),
mcp.Description("要执行的算术运算类型"),
mcp.Enum("add", "subtract", "multiply", "divide"), // 保持英文
),
mcp.WithNumber("x",
mcp.Required(),
mcp.Description("第一个数字"),
),
mcp.WithNumber("y",
mcp.Required(),
mcp.Description("第二个数字"),
),
)
s.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
op := request.Params.Arguments["operation"].(string)
x := request.Params.Arguments["x"].(float64)
y := request.Params.Arguments["y"].(float64)
var result float64
switch op {
case "add":
result = x + y
case "subtract":
result = x - y
case "multiply":
result = x * y
case "divide":
if y == 0 {
return nil, errors.New("不允许除以零")
}
result = x / y
}
fmt.Printf("操作类型: %s, 参数 x: %f, 参数 y: %f\n", op, x, y)
return mcp.FormatNumberResult(result), nil
})
}
// 添加资源
{
// 静态资源示例 - 暴露一个 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")
fmt.Printf("请求的资源 URI: %s\n", request.Request)
if err != nil {
return nil, err
}
return []mcp.ResourceContents{
mcp.TextResourceContents{
URI: "docs://readme",
MIMEType: "text/markdown",
Text: string(content),
},
}, nil
})
}
// 添加提示词
{
// 简单问候提示
s.AddPrompt(mcp.NewPrompt("greeting",
mcp.WithPromptDescription("一个友好的问候提示"),
mcp.WithArgument("name",
mcp.ArgumentDescription("要问候的人的名字"),
),
), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
name := request.Params.Arguments["name"]
if name == "" {
name = "朋友"
}
return mcp.NewGetPromptResult(
"友好的问候",
[]mcp.PromptMessage{
mcp.NewPromptMessage(
mcp.RoleAssistant,
mcp.NewTextContent(fmt.Sprintf("你好,%s!今天有什么可以帮您的吗?", name)),
),
},
), nil
})
}
// 启动MCP服务器,使用标准输入输出作为通信方式
// if err := server.ServeStdio(s); err != nil {
// fmt.Printf("Server error: %v\n", err)
// }
// 启动MCP服务器,使用HTTP作为通信方式
// 创建基于 SSE 的服务器实例
sseServer := server.NewSSEServer(s)
// 启动服务器,监听指定端口(如 :8080)
err := sseServer.Start(":8080")
if err != nil {
panic(err)
}
}
客户端
Stdio方式的代码,SSE的还有问题。
package main
import (
"context"
"fmt"
"time"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
)
func main() {
// 创建一个基于 stdio 的MCP客户端
mcpClient, err := client.NewStdioMCPClient(
"./mcp.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()
fmt.Println("资源列表:")
resourcesRequest := mcp.ListResourcesRequest{}
resources, err := mcpClient.ListResources(ctx, resourcesRequest)
if err != nil {
panic(err)
}
for _, resource := range resources.Resources {
fmt.Printf("- uri: %s, name: %s, description: %s, MIME类型: %s\n", resource.URI, resource.Name, resource.Description, resource.MIMEType)
}
// 从服务器获取工具列表
fmt.Println()
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()
// 调用工具
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": 9,
"y": 1,
}
// Call the tool
result, err := mcpClient.CallTool(ctx, toolRequest)
if err != nil {
panic(err)
}
fmt.Println("调用工具结果:", result.Content[0].(mcp.TextContent).Text)
}
命令行返回信息:
初始化 mcp 客户端...
初始化成功,服务器信息: 我的计算器 1.0.0
提示词列表:
- greeting: 一个友好的问候提示
参数: [{name 要问候的人的名字 false}]
资源列表:
- uri: docs://readme, name: 项目说明文档, description: 项目的 README 文件, MIME类型: text/markdown
可用工具列表:
- calculate: 执行基本的算术运算
参数: map[operation:map[description:要执行的算术运算类型 enum:[add subtract multiply divide] type:string] x:map[description:第一个数字 type:number] y:map[description:第二个数字 type:number]]
调用工具: calculate
调用工具结果: 10.00
3 模仿
通过模仿,飞快的实现了在VSCode中,通过文字向ntfy发送消息。
进一步的思考整个流程:语音->文字识别->AI意图识别->MCP->请求完成->语音反馈
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/ThinkInAIXYZ/go-mcp/protocol"
"github.com/ThinkInAIXYZ/go-mcp/server"
"github.com/ThinkInAIXYZ/go-mcp/transport"
)
type ntfyReq struct {
Object string `json:"object" description:"接收对象"`
Message string `json:"message" description:"消息内容"`
}
// 中间件函数
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("收到请求: %s %s %s", r.RemoteAddr, r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
func main() {
var (
messageUrl = "/message"
port = "80"
)
// 创建传输服务器 (SSE)
transport, handler, err := transport.NewSSEServerTransportAndHandler(fmt.Sprintf("http://127.0.0.1:%s%s", port, messageUrl))
if err != nil {
log.Fatalf("创建SSE传输失败: %v", err)
}
mcpServer, err := server.NewServer(transport,
server.WithServerInfo(protocol.Implementation{
Name: "Ease实用工具-MCP服务器",
Version: "0.0.1",
}),
)
if err != nil {
log.Fatalf("创建MCP服务失败: %v", err)
}
// 注册工具句柄:名称,描述,请求参数
ntfy, err := protocol.NewTool("ntfy message", "向ntfy发送消息", ntfyReq{})
if err != nil {
log.Fatalf("创建工具失败: %v", err)
return
}
// 新的工具句柄和返回值
ntfyHandler := func(request *protocol.CallToolRequest) (*protocol.CallToolResult, error) {
req := new(ntfyReq)
if err := json.Unmarshal(request.RawArguments, &req); err != nil {
return nil, err
}
object := req.Object
message := req.Message
if object == "" {
return nil, fmt.Errorf("消息对象不能为空")
}
if message == "" {
return nil, fmt.Errorf("信息不能为空")
}
err := sendNtfyNotification(object, message)
if err != nil {
return nil, fmt.Errorf("发送ntfy消息失败: %v", err)
}
return &protocol.CallToolResult{
Content: []protocol.Content{
protocol.TextContent{
Type: "text",
Text: fmt.Sprintf("向ntfy发送消息: %s 完成", req.Message),
},
},
}, nil
}
mcpServer.RegisterTool(ntfy, ntfyHandler)
// 设置 HTTP 路由
http.Handle("/sse", middleware(handler.HandleSSE()))
http.Handle(messageUrl, middleware(handler.HandleMessage()))
http.Handle("/", middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte("<h1>欢迎使用MCP服务器</h1><p>请访问 <a href='/sse'>/sse</a> 来查看SSE消息。</p>")) // 返回简单的HTML页面
})))
// 启动 HTTP 服务器
fmt.Println("启动MCP服务器...")
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatalf("启动HTTP服务失败: %v", err)
}
}
改进
通过参数,即可以stdio,也可以sse。虽然看起来不够漂亮。
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"github.com/ThinkInAIXYZ/go-mcp/protocol"
"github.com/ThinkInAIXYZ/go-mcp/server"
"github.com/ThinkInAIXYZ/go-mcp/transport"
)
type ntfyReq struct {
Object string `json:"object" description:"接收对象"`
Message string `json:"message" description:"消息内容"`
}
var (
messageUrl = "/message"
port = "80"
mode string
)
// 中间件函数
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("收到请求: %s %s %s", r.RemoteAddr, r.Method, r.URL.Path)
// 打印请求体
var body []byte
r.Body.Read(body)
log.Printf("请求体: %s", string(body))
next.ServeHTTP(w, r)
})
}
func init() {
flag.StringVar(&mode, "t", "sse", "MCP服务器传输模式: stdio 或 sse")
flag.Parse()
}
func main() {
t, handler, err := getTransport()
mcpServer, err := server.NewServer(t,
server.WithServerInfo(protocol.Implementation{
Name: "Ease实用工具-MCP服务器",
Version: "0.0.1",
}),
)
if err != nil {
log.Fatalf("创建MCP服务失败: %v", err)
}
// 注册工具句柄:名称,描述,请求参数
ntfy, err := protocol.NewTool("ntfy message", "向ntfy发送消息", ntfyReq{})
if err != nil {
log.Fatalf("创建工具失败: %v", err)
return
}
// 新的工具句柄和返回值
ntfyHandler := func(request *protocol.CallToolRequest) (*protocol.CallToolResult, error) {
req := new(ntfyReq)
if err := json.Unmarshal(request.RawArguments, &req); err != nil {
return nil, err
}
object := req.Object
message := req.Message
if object == "" {
return nil, fmt.Errorf("消息对象不能为空")
}
if message == "" {
return nil, fmt.Errorf("信息不能为空")
}
err := sendNtfyNotification(object, message)
if err != nil {
return nil, fmt.Errorf("发送ntfy消息失败: %v", err)
}
return &protocol.CallToolResult{
Content: []protocol.Content{
protocol.TextContent{
Type: "text",
Text: fmt.Sprintf("向ntfy发送消息: %s 完成", req.Message),
},
},
}, nil
}
mcpServer.RegisterTool(ntfy, ntfyHandler)
if mode == "stdio" {
log.Println("启动MCP服务器(Stdio)...")
mcpServer.Run()
} else {
// 设置 HTTP 路由
http.Handle("/sse", middleware(handler.HandleSSE()))
http.Handle(messageUrl, middleware(handler.HandleMessage()))
http.Handle("/", middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte("<h1>欢迎使用MCP服务器</h1><p>请访问 <a href='/sse'>/sse</a> 来查看SSE消息。</p>")) // 返回简单的HTML页面
})))
// 启动 HTTP 服务器
log.Print("启动MCP服务器(SSE)...")
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatalf("启动HTTP服务失败: %v", err)
}
}
}
func getTransport() (t transport.ServerTransport, h *transport.SSEHandler, err error) {
if mode == "stdio" {
t = transport.NewStdioServerTransport()
} else {
// 创建传输服务器 (SSE)
t, h, err = transport.NewSSEServerTransportAndHandler(fmt.Sprintf("http://127.0.0.1:%s%s", port, messageUrl))
if err != nil {
log.Fatalf("创建SSE传输失败: %v", err)
}
}
return
}