(摘) 关注MCP AI模型交互协议

声明:内容源自网络,版权归原作者所有。若有侵权请在网页聊天中联系我

据说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
}

相关文章