├── README.md ├── code-examples └── overview │ ├── client │ ├── client.go │ └── server │ ├── go.mod │ ├── go.sum │ └── server │ └── server.go └── tutorials ├── MCP 协议更新详解:从 HTTP+SSE 到 Streamable HTTP └── MCP 协议更新详解:从 HTTP+SSE 到 Streamable HTTP.md ├── MCP 实战:用 Go 语言开发一个查询 IP 信息的 MCP 服务器 └── MCP 实战:用 Go 语言开发一个查询 IP 信息的 MCP 服务器.md ├── 一文掌握 MCP 上下文协议:从理论到实践 ├── images │ ├── 1.png │ ├── 2.png │ └── 3.png └── 一文掌握 MCP 上下文协议:从理论到实践.md ├── 三句话搞定周末出行攻略!我用 AI 生成一日游可视化页面,还能秒上线! └── 三句话搞定周末出行攻略!我用 AI 生成一日游可视化页面,还能秒上线!.md └── 用 Go 语言轻松构建 MCP 客户端与服务器 └── 用 Go 语言轻松构建 MCP 客户端与服务器.md /README.md: -------------------------------------------------------------------------------- 1 | # Model Context Protocol(MCP) 中文教程讲解 2 | 3 | 大家好,我是陈明勇。本仓库将会讲解 `Model Context Protocol`(`MCP`) 的中文教程。`MCP` 是一种开放标准,旨在标准化大型语言模型(`LLM`)与外部数据源和工具之间的交互方式。 4 | 5 | # 文章目录 6 | 7 | - [一文掌握 MCP 上下文协议:从理论到实践](./tutorials/一文掌握%20MCP%20上下文协议:从理论到实践/一文掌握%20MCP%20上下文协议:从理论到实践.md):万字长文讲解 模型上下文协议(`Model Context Protocol`,`MCP`),通过本文,你可以快速了解 `MCP` 相关的概念,并通过阅读代码示例进一步加深对 `MCP` 的掌握。 8 | - [用 Go 语言轻松构建 MCP 客户端与服务器](./tutorials/用%20Go%20语言轻松构建%20MCP%20客户端与服务器/用%20Go%20语言轻松构建%20MCP%20客户端与服务器.md):随着 `MCP` 越来越受欢迎,`Go MCP` 库应运而生。本文将介绍如何在 `Go` 语言里面轻松构建 `MCP` 客户端和服务器。 9 | - [MCP 协议更新详解:从 HTTP+SSE 到 Streamable HTTP](./tutorials/MCP%20协议更新详解:从%20HTTP+SSE%20到%20Streamable%20HTTP/MCP%20协议更新详解:从%20HTTP+SSE%20到%20Streamable%20HTTP.md):`Streamable HTTP` 是对 `MCP` 协议传输层的一次重要优化。它在保留原有 `HTTP + SSE` 模式优势的基础上,解决了连接不可恢复、长连接负担重、传输不灵活等问题,带来了更高的可用性与灵活性。 10 | - [三句话搞定周末出行攻略!我用 AI 生成一日游可视化页面,还能秒上线!](./tutorials/三句话搞定周末出行攻略!我用%20AI%20生成一日游可视化页面,还能秒上线!/三句话搞定周末出行攻略!我用%20AI%20生成一日游可视化页面,还能秒上线!.md):本文将介绍如何结合 `AI` 与高德地图、`EdgeOne Pages Deploy` 提供的 `MCP Server`,通过三句话即可生成一份可视化的一日游行程页面,并部署到 `EdgeOne` 上以便在线访问。 11 | - [MCP 实战:用 Go 语言开发一个查询 IP 信息的 MCP 服务器](./tutorials/MCP%20实战:用%20Go%20语言开发一个查询%20IP%20信息的%20MCP%20服务器/MCP%20实战:用%20Go%20语言开发一个查询%20IP%20信息的%20MCP%20服务器.md):随着 `MCP` 的快速普及和广泛应用,`MCP` 服务器也层出不穷。大多数开发者使用的 `MCP` 服务器开发库是官方提供的 `typescript-sdk`,而作为 `Go` 开发者,我们也可以借助优秀的第三方库去开发 `MCP` 服务器,例如 `ThinkInAIXYZ/go-mcp`。 本文将详细介绍如何在 `Go` 语言中使用 `go-mcp` 库来开发一个查询 `IP` 信息的 `MCP` 服务器。 12 | 13 | 14 | # 学习与交流 15 | 一般会先在公众号上更新文章,再同步到站点里。成功的路上并不拥挤,有没有兴趣结个伴?欢迎加我微信:`chenmingyong1999`,或关注公众号: 16 | 17 | ![微信公众号:程序员陈明勇](https://raw.githubusercontent.com/chenmingyong0423/blog/master/images/wx-gzh-qrcode.jpeg) 18 | 19 | # 感谢支持 20 | 如果您能在阅读文章的过程中有所收获,哪怕一丁点,对于本人来说是莫大的鼓励;欢迎 `Star`,给予作者最大的鼓励。 21 | 22 | # 赞赏 23 | 写作不易,如果教程能给你带来收获和帮助,打个赏可好?金额随意,感谢您!打赏时请务必备注来源以及您的称呼。 24 | 25 |
26 | 陈明勇的微信二维码 27 | 陈明勇的支付宝二维码 28 |
29 | 30 | # 版权声明 31 | 所有文章采用 [署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh) 进行许可。 32 | -------------------------------------------------------------------------------- /code-examples/overview/client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/mark3labs/mcp-go/client" 9 | "github.com/mark3labs/mcp-go/mcp" 10 | ) 11 | 12 | func main() { 13 | 14 | // 创建一个基于 stdio 的MCP客户端 15 | mcpClient, err := client.NewStdioMCPClient( 16 | "./client/server", 17 | []string{}, 18 | ) 19 | if err != nil { 20 | panic(err) 21 | } 22 | defer mcpClient.Close() 23 | 24 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 25 | defer cancel() 26 | 27 | fmt.Println("初始化 mcp 客户端...") 28 | initRequest := mcp.InitializeRequest{} 29 | initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION 30 | initRequest.Params.ClientInfo = mcp.Implementation{ 31 | Name: "Client Demo", 32 | Version: "1.0.0", 33 | } 34 | 35 | // 初始化MCP客户端并连接到服务器 36 | initResult, err := mcpClient.Initialize(ctx, initRequest) 37 | if err != nil { 38 | panic(err) 39 | } 40 | fmt.Printf( 41 | "\n初始化成功,服务器信息: %s %s\n\n", 42 | initResult.ServerInfo.Name, 43 | initResult.ServerInfo.Version, 44 | ) 45 | 46 | // 从服务器获取提示词列表 47 | fmt.Println("提示词列表:") 48 | promptsRequest := mcp.ListPromptsRequest{} 49 | prompts, err := mcpClient.ListPrompts(ctx, promptsRequest) 50 | if err != nil { 51 | panic(err) 52 | } 53 | for _, prompt := range prompts.Prompts { 54 | fmt.Printf("- %s: %s\n", prompt.Name, prompt.Description) 55 | fmt.Println("参数:", prompt.Arguments) 56 | } 57 | 58 | // 从服务器获取资源列表 59 | fmt.Println() 60 | fmt.Println("资源列表:") 61 | resourcesRequest := mcp.ListResourcesRequest{} 62 | resources, err := mcpClient.ListResources(ctx, resourcesRequest) 63 | if err != nil { 64 | panic(err) 65 | } 66 | for _, resource := range resources.Resources { 67 | fmt.Printf("- uri: %s, name: %s, description: %s, MIME类型: %s\n", resource.URI, resource.Name, resource.Description, resource.MIMEType) 68 | } 69 | 70 | // 从服务器获取工具列表 71 | fmt.Println() 72 | fmt.Println("可用工具列表:") 73 | toolsRequest := mcp.ListToolsRequest{} 74 | tools, err := mcpClient.ListTools(ctx, toolsRequest) 75 | if err != nil { 76 | panic(err) 77 | } 78 | 79 | for _, tool := range tools.Tools { 80 | fmt.Printf("- %s: %s\n", tool.Name, tool.Description) 81 | fmt.Println("参数:", tool.InputSchema.Properties) 82 | } 83 | fmt.Println() 84 | 85 | // 调用工具 86 | fmt.Println("调用工具: calculate") 87 | toolRequest := mcp.CallToolRequest{ 88 | Request: mcp.Request{ 89 | Method: "tools/call", 90 | }, 91 | } 92 | toolRequest.Params.Name = "calculate" 93 | toolRequest.Params.Arguments = map[string]any{ 94 | "operation": "add", 95 | "x": 1, 96 | "y": 1, 97 | } 98 | // Call the tool 99 | result, err := mcpClient.CallTool(ctx, toolRequest) 100 | if err != nil { 101 | panic(err) 102 | } 103 | fmt.Println("调用工具结果:", result.Content[0].(mcp.TextContent).Text) 104 | } 105 | -------------------------------------------------------------------------------- /code-examples/overview/client/server: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenmingyong0423/mcp-tutorials/1f79733f6c34c81500983e19aa5a739b6734b0d8/code-examples/overview/client/server -------------------------------------------------------------------------------- /code-examples/overview/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chenmingyong0423/mcp-tutorials/code-examples/overview 2 | 3 | go 1.24.0 4 | 5 | require github.com/mark3labs/mcp-go v0.17.0 6 | 7 | require ( 8 | github.com/google/uuid v1.6.0 // indirect 9 | github.com/yosida95/uritemplate/v3 v3.0.2 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /code-examples/overview/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/mark3labs/mcp-go v0.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi930= 6 | github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 10 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 11 | github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= 12 | github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /code-examples/overview/server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/mark3labs/mcp-go/mcp" 10 | "github.com/mark3labs/mcp-go/server" 11 | ) 12 | 13 | func main() { 14 | s := server.NewMCPServer( 15 | "Server Demo", 16 | "1.0.0", 17 | ) 18 | 19 | // 添加工具 20 | { 21 | calculatorTool := mcp.NewTool("calculate", 22 | mcp.WithDescription("执行基本的算术运算"), 23 | mcp.WithString("operation", 24 | mcp.Required(), 25 | mcp.Description("要执行的算术运算类型"), 26 | mcp.Enum("add", "subtract", "multiply", "divide"), // 保持英文 27 | ), 28 | mcp.WithNumber("x", 29 | mcp.Required(), 30 | mcp.Description("第一个数字"), 31 | ), 32 | mcp.WithNumber("y", 33 | mcp.Required(), 34 | mcp.Description("第二个数字"), 35 | ), 36 | ) 37 | 38 | s.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 39 | op := request.Params.Arguments["operation"].(string) 40 | x := request.Params.Arguments["x"].(float64) 41 | y := request.Params.Arguments["y"].(float64) 42 | 43 | var result float64 44 | switch op { 45 | case "add": 46 | result = x + y 47 | case "subtract": 48 | result = x - y 49 | case "multiply": 50 | result = x * y 51 | case "divide": 52 | if y == 0 { 53 | return nil, errors.New("不允许除以零") 54 | } 55 | result = x / y 56 | } 57 | 58 | return mcp.FormatNumberResult(result), nil 59 | }) 60 | } 61 | 62 | // 添加资源 63 | { 64 | // 静态资源示例 - 暴露一个 README 文件 65 | resource := mcp.NewResource( 66 | "docs://readme", 67 | "项目说明文档", 68 | mcp.WithResourceDescription("项目的 README 文件"), 69 | mcp.WithMIMEType("text/markdown"), 70 | ) 71 | 72 | // 添加资源及其处理函数 73 | s.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { 74 | content, err := os.ReadFile("README.md") 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | return []mcp.ResourceContents{ 80 | mcp.TextResourceContents{ 81 | URI: "docs://readme", 82 | MIMEType: "text/markdown", 83 | Text: string(content), 84 | }, 85 | }, nil 86 | }) 87 | } 88 | 89 | // 添加提示词 90 | { 91 | // 简单问候提示 92 | s.AddPrompt(mcp.NewPrompt("greeting", 93 | mcp.WithPromptDescription("一个友好的问候提示"), 94 | mcp.WithArgument("name", 95 | mcp.ArgumentDescription("要问候的人的名字"), 96 | ), 97 | ), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { 98 | name := request.Params.Arguments["name"] 99 | if name == "" { 100 | name = "朋友" 101 | } 102 | 103 | return mcp.NewGetPromptResult( 104 | "友好的问候", 105 | []mcp.PromptMessage{ 106 | mcp.NewPromptMessage( 107 | mcp.RoleAssistant, 108 | mcp.NewTextContent(fmt.Sprintf("你好,%s!今天有什么可以帮您的吗?", name)), 109 | ), 110 | }, 111 | ), nil 112 | }) 113 | } 114 | 115 | // 启动基于 stdio 的服务器 116 | if err := server.ServeStdio(s); err != nil { 117 | fmt.Printf("Server error: %v\n", err) 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /tutorials/MCP 协议更新详解:从 HTTP+SSE 到 Streamable HTTP/MCP 协议更新详解:从 HTTP+SSE 到 Streamable HTTP.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | ![image](https://github.com/user-attachments/assets/f020ec78-6c4d-4012-9830-b8ea34c6e0e9) 4 | 5 | **2025 年 3 月 26 日**,模型上下文协议(`Model Context Protocol`,简称 `MCP`)引入了一项关键更新:用 `Streamable HTTP` 替代原先的 `HTTP + SSE` 作为默认传输方式。 6 | 7 | 这一变更在解决原有方案中连接不可恢复、服务端长连接压力大等问题的同时,依然保留了 `SSE` 带来的流式响应优势。 8 | 9 | 本文将深入解析这次更新背后的动因、技术细节以及实际应用场景。 10 | 11 | 准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。 12 | 13 | ![image](https://github.com/user-attachments/assets/dc868416-76ea-4c33-a886-31420cdd1a52) 14 | 15 | # HTTP + SSE 的缺陷 16 | 17 | 远程 `MCP` 通过 `HTTP + SSE` 的传输方式工作,存在以下问题,这也是它所被替换的根本原因: 18 | 19 | * 不支持恢复连接 20 | * 要求服务器保持高可用的长连接 21 | * 服务器只能通过 `SSE` 发送消息 22 | 23 | ## 不支持恢复连接 24 | 25 | 如果客户端和服务器之间的 `SSE` 连接中断了,就无法 “从端点继续”,只能重新开始新的连接,之前的上下文可能会丢失。 26 | 27 | ## 要求服务器保持高可用的长连接 28 | 29 | 服务器必须一直保持一个稳定、不中断的 `SSE` 长连接,否则通信就中断。 30 | 31 | ## 服务器只能通过 `SSE` 发送消息 32 | 33 | 服务器无法在已有的请求之外,主动地发送消息给客户端,除了通过专门的 /sse 通道。换句话说,它是“单向被动响应”,而不是“任意时机推送”。 34 | 35 | # Streamable HTTP 36 | 37 | `Streamable HTTP` 并不是传统意义上的 **流式 HTTP**(`Streaming HTTP`),它指的是一种 **兼具以下特性的传输机制**: 38 | 39 | * 以普通 `HTTP` 请求为基础,客户端用 `POST/GET` 发请求; 40 | 41 | * 服务器可选地将响应升级为 `SSE` 流,实现 **流式传输** 的能力(当需要时); 42 | 43 | * 去中心化、无强制要求持续连接,支持 `stateless` 模式; 44 | 45 | * 客户端和服务端之间的消息传输更加灵活,比如同一个 `/message` 端点可用于发起请求和接收 `SSE` 流; 46 | 47 | * 不再需要单独的 `/sse` 端点,一切通过统一的 `/message` 协议层处理。 48 | 49 | ## Streamable HTTP 的优势 50 | 51 | * 支持无状态服务器:无需维持高可用的长连接 52 | 53 | * 纯 `HTTP` 实现:`MCP` 可在纯 `HTTP` 服务中实现,无需 `SSE` 支持 54 | 55 | * 兼容基础设施:因为 “只是 HTTP”,可以与中间件和现有基础设施良好集成 56 | 57 | * 向后兼容:是当前 `HTTP+SSE` 传输方式的渐进式改进 58 | 59 | * 灵活的传输方式:服务器可选择是否使用 `SSE` 进行流式响应 60 | 61 | ## 从 HTTP+SSE 到 Streamable HTTP 的变化 62 | 63 | * 移除了 `/sse` 端点 64 | 65 | * 所有客户端 → 服务端的消息都通过 `/message`(或类似端点)发送 66 | 67 | * 所有客户端 → 服务端的请求都可以被服务器升级为 `SSE`,以发送通知或请求 68 | 69 | * 服务器可以选择建立会话 `ID` 以维护状态 70 | 71 | * 客户端可以通过对 `/message` 发送一个空的 `GET` 请求启动 `SSE` 流 72 | 73 | * 该方法兼容旧版本的实现,并允许服务器保持无状态(如果有需要) 74 | 75 | # 为什么不用 WebSocket? 76 | 77 | 官方团队曾认真探讨过是否应该将 `WebSocket` 作为远程通信的主要方式,并尝试在其基础上实现断线重连等功能。但最终决定暂时不采用 `WebSocket`,主要原因如下: 78 | 79 | * 想要以 `RPC` 风格使用 `MCP`(例如构建一个无状态、只暴露基础工具的服务)时,如果每次调用都依赖 `WebSocket`,将引入不必要的维护和网络开销。 80 | 81 | * 在浏览器环境中,`WebSocket` 连接无法像普通 `HTTP` 请求那样附加请求头(比如 `Authorization`),而且不同于 `SSE`,`WebSocket` 在浏览器中也无法由第三方库完全“模拟”实现。 82 | 83 | * 只有 `GET` 请求可以自动升级为 `WebSocket`,而 `POST` 等其他 `HTTP` 方法并不支持直接升级。这就意味着如果要让 `POST` 请求使用 `WebSocket`,需要一个额外的 **两步升级** 过程,增加了实现的复杂度和延迟。 84 | 85 | * 也避免在 `MCP` 规范中增加 `WebSocket` 的可选支持,以减少客户端与服务器间可能的兼容性组合问题(但不阻止社区自己扩展非官方的 `WebSocket` 实现) 86 | 87 | * 官方也有意避免在 `MCP` 协议中引入 `WebSocket` 作为官方选项,以避免客户端和服务器之间因传输方式组合过多而导致的兼容性问题(当然,这并不妨碍用户基于 `WebSocket` 自行实现非官方版本)。 88 | 89 | 当然,如果将来实践中发现 `SSE` 并不理想,官方仍会考虑重新评估 `WebSocket` 的可能性。 90 | 91 | # MCP Server 示例 92 | 93 | ## 无状态服务器(Stateless Server) 94 | 95 | `Streamable HTTP` 支持构建完全无状态、无需保持长连接的服务器架构。 96 | 97 | 以一个仅提供大语言模型(`LLM`)工具的服务为例,不依赖其他高级功能,可以按以下方式实现: 98 | 99 | * 始终响应初始化请求,但无需持久化任何状态; 100 | * 对所有传入的 `ToolListRequest`,直接返回一个标准的 `JSON-RPC` 响应; 101 | * 对 `CallToolRequest`,执行对应工具,等待其完成后,将结果通过 `HTTP` 响应体以 `CallToolResponse` 的形式返回。 102 | 103 | ## 支持流式输出的无状态服务器(Stateless Server with Streaming) 104 | 105 | 即使服务器完全无状态、且不支持长连接,也仍然可以利用此设计进行流式响应。 106 | 107 | 以工具调用时的进度反馈为例: 108 | 109 | * 当收到 `POST` 的 `CallToolRequest` 时,服务器通过响应头声明该响应将以 `SSE`(`Server-Sent Events`)格式返回; 110 | * 启动工具执行逻辑; 111 | * 在执行过程中,服务器可以通过 `SSE` 向客户端持续发送多个 `ProgressNotification`(进度通知); 112 | * 执行完成后,服务器通过 `SSE` 发送最终的 `CallToolResponse`; 113 | * 最后,服务器关闭 `SSE` 流,整个交互完成。 114 | 115 | ## 有状态服务器(Stateful Server) 116 | 117 | 对于需要维护客户端会话的服务器,整体架构可以保持与 `http+SSE` 的实现类似。 118 | 119 | 主要区别在于:服务器需要为客户端生成唯一的会话 `ID`,并要求客户端在后续所有请求中携带该 `ID`。 120 | 121 | 服务器可以利用会话 `ID` 实现 **粘性路由** 或消息总线中的会话定位。例如,在水平扩展的部署中(部署多台相同的 `mcp server`),某个 `POST` 请求可能被路由到任意一个节点,此时可以通过 `Redis` 等中间件将请求路由到关联的会话上下文,确保状态一致性。 122 | 123 | # 小结 124 | 125 | `Streamable HTTP` 是对 `MCP` 协议传输层的一次重要优化。它在保留原有 `HTTP + SSE` 模式优势的基础上,解决了连接不可恢复、长连接负担重、传输不灵活等问题,带来了更高的可用性与灵活性。 126 | -------------------------------------------------------------------------------- /tutorials/MCP 实战:用 Go 语言开发一个查询 IP 信息的 MCP 服务器/MCP 实战:用 Go 语言开发一个查询 IP 信息的 MCP 服务器.md: -------------------------------------------------------------------------------- 1 | # 📝 前言 2 | 3 | 随着 `MCP` 的快速普及和广泛应用,`MCP` 服务器也层出不穷。大多数开发者使用的 `MCP` 服务器开发库是官方提供的 `typescript-sdk`,而作为 `Go` 开发者,我们也可以借助优秀的第三方库去开发 `MCP` 服务器,例如 `ThinkInAIXYZ/go-mcp`。 4 | 5 | 本文将详细介绍如何在 `Go` 语言中使用 `go-mcp` 库来开发一个查询 `IP` 信息的 `MCP` 服务器。 6 | 7 | 准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。 8 | 9 | ![image](https://github.com/user-attachments/assets/fef15469-89d4-4d0a-ae08-143ec0a31396) 10 | 11 | 12 | # 🌐 mcp-ip-geo 服务器 13 | 14 | `mcp-ip-geo` 是一个用于查询 `IP` 信息的 `MCP` 服务器,项目已开源,仓库地址:[mcp-ip-geo](https://github.com/chenmingyong0423/mcp-ip-geo)。 15 | 16 | ## 📁 目录结构说明 17 | 18 | ├─cmd 19 | │ └─mcp-ip-geo 20 | └─internal 21 | ├─domain 22 | ├─server 23 | ├─service 24 | └─tools 25 | 26 | * `cmd/mcp-ip-geo`:应用的启动入口目录,包含如 `main.go` 启动文件。 27 | * `internal/domain`:定义项目中的核心数据结构,例如 `IP API` 的响应体等。 28 | * `internal/server`:`MCP` 服务器的核心逻辑实现。 29 | * `internal/service`:对接第三方服务的实现,如调用 `IP` 解析 `API`。 30 | * `internal/tools`:`MCP` 工具的具体实现,支持灵活扩展和注册。 31 | 32 | # 🔍 查询 IP 信息功能实现 33 | 34 | 代码位于 `service` 包中,通过 `ip-api.com` 提供的接口获取 `IP` 地理位置信息,具体实现如下: 35 | 36 | ```go 37 | package service 38 | 39 | import ( 40 | "context" 41 | "fmt" 42 | "github.com/chenmingyong0423/mcp-ip-geo/internal/domain" 43 | "net/http" 44 | "time" 45 | 46 | httpchain "github.com/chenmingyong0423/go-http-chain" 47 | ) 48 | 49 | func NewIpApiService() *IpApiService { 50 | return &IpApiService{ 51 | host: "http://ip-api.com", 52 | client: httpchain.NewWithClient(&http.Client{ 53 | Timeout: time.Second * 10, 54 | }), 55 | } 56 | } 57 | 58 | type IIpApiService interface { 59 | GetLocation(ctx context.Context, ip string) (*domain.IpApiResponse, error) 60 | } 61 | 62 | var _ IIpApiService = (*IpApiService)(nil) 63 | 64 | type IpApiService struct { 65 | host string 66 | client *httpchain.Client 67 | } 68 | 69 | func (s *IpApiService) GetLocation(ctx context.Context, ip string) (*domain.IpApiResponse, error) { 70 | var resp domain.IpApiResponse 71 | err := s.client.Get(fmt.Sprintf("%s/json/%s", s.host, ip)).DoAndParse(ctx, &resp) 72 | if err != nil { 73 | return nil, err 74 | } 75 | return &resp, nil 76 | } 77 | 78 | ``` 79 | 80 | **代码解释:** 81 | 82 | * 服务初始化(`NewIpApiService`) 83 | * 创建一个新的 `IpApiService` 实例。 84 | * 设置了 `API` 地址为 `http://ip-api.com`。 85 | * 使用 `httpchain` 封装的 `HTTP` 客户端,设置请求超时时间为 10 秒。 86 | 87 | * 接口定义(`IIpApiService`) 88 | * 定义了服务对外暴露的功能:`GetLocation` 方法,用于获取 `IP` 地理位置信息。   89 | * 使用接口有助于后续做依赖注入、`mock` 测试等。   90 | * `var _ IIpApiService = (*IpApiService)(nil)` 这行代码用于编译时检查,确保 `IpApiService` 实现了 `IIpApiService` 接口。 91 | 92 | * 结构体定义(`IpApiService`)  93 | * 包含两个字段: 94 | * `host`:`API` 的基础地址。 95 | * `client`:封装的 `HTTP` 客户端,类型为 `*httpchain.Client`。 96 | 97 | * 核心方法实现(`GetLocation`) 98 | * 根据传入的 `IP` 构造请求地址:`http://ip-api.com/json/{ip}`。   99 | * 使用 `httpchain` 库发起 `GET` 请求,并将结果解析到 `domain.IpApiResponse` 结构体中。   100 | 101 | # 🧰 工具实现 102 | 103 | ## 🔧 工具管理 104 | 105 | 代码位于 `tools` 包中,用于管理工具,具体实现如下: 106 | 107 | ```go 108 | package tools 109 | 110 | import ( 111 | "github.com/ThinkInAIXYZ/go-mcp/protocol" 112 | "github.com/ThinkInAIXYZ/go-mcp/server" 113 | ) 114 | 115 | type ToolFunc func() (tool *protocol.Tool, toolHandler server.ToolHandlerFunc) 116 | 117 | func GetToolFuncList() []ToolFunc { 118 | return []ToolFunc{ 119 | SingleIpParser, 120 | } 121 | } 122 | 123 | ``` 124 | 125 | 代码解释: 126 | 127 | * **`ToolFunc` 类型定义** 128 | 129 | * 定义了一个函数类型 `ToolFunc`,返回两个值: 130 | 131 | * `*protocol.Tool`:工具的元信息; 132 | * `server.ToolHandlerFunc`:该工具的处理逻辑函数。 133 | 134 | * 用这种方式可以将 **工具的定义** 与 **工具的执行逻辑** 一并管理,后续在定义工具时都可以通过实现该函数签名进行表示。 135 | 136 | * **`GetToolFuncList` 函数** 137 | 138 | * 返回一个 `ToolFunc` 列表。 139 | * 当前只注册了一个工具:`SingleIpParser`,但这种结构易于扩展,后续只需往列表中添加新的工具函数即可。 140 | * 通过集中注册,应用在初始化时可以统一加载所有工具。 141 | 142 | ## 🌍 查询单个 IP 信息工具的实现 143 | 144 | 代码位于 `tools` 包中,用于查询单个 `IP` 信息,具体实现如下: 145 | 146 | ```go 147 | package tools 148 | 149 | import ( 150 | "context" 151 | "encoding/json" 152 | "github.com/ThinkInAIXYZ/go-mcp/protocol" 153 | "github.com/ThinkInAIXYZ/go-mcp/server" 154 | "github.com/chenmingyong0423/mcp-ip-geo/internal/service" 155 | ) 156 | 157 | var singleIpParserTool *protocol.Tool 158 | 159 | type ipRequest struct { 160 | Ip string `json:"ip"` 161 | } 162 | 163 | func init() { 164 | var err error 165 | singleIpParserTool, err = protocol.NewTool("ip-details", "a tool that provides IP geolocation information", ipRequest{}) 166 | if err != nil { 167 | panic(err) 168 | } 169 | } 170 | 171 | func SingleIpParser() (*protocol.Tool, server.ToolHandlerFunc) { 172 | ipApiService := service.NewIpApiService() 173 | 174 | return singleIpParserTool, func(toolRequest *protocol.CallToolRequest) (*protocol.CallToolResult, error) { 175 | var req ipRequest 176 | if err := protocol.VerifyAndUnmarshal(toolRequest.RawArguments, &req); err != nil { 177 | return nil, err 178 | } 179 | resp, err := ipApiService.GetLocation(context.Background(), req.Ip) 180 | if err != nil { 181 | return nil, err 182 | } 183 | 184 | marshal, err := json.Marshal(resp) 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | return &protocol.CallToolResult{ 190 | Content: []protocol.Content{ 191 | protocol.TextContent{ 192 | Type: "text", 193 | Text: string(marshal), 194 | }, 195 | }, 196 | }, nil 197 | } 198 | } 199 | 200 | ``` 201 | 202 | **代码解释:** 203 | 204 | * **全局变量声明** 205 | 206 | * `singleIpParserTool`:存储工具元信息的协议工具对象 207 | * `ipRequest`:定义工具输入参数结构体,包含 `ip` 字符串字段 208 | 209 | * **初始化函数(`init`)** 210 | 211 | * 在包加载时通过 `protocol.NewTool` 创建工具元信息 212 | * 指定工具标识符 `ip-details`,描述信息和输入参数结构体 `ipRequest{}` 213 | * 错误处理采用 `panic`,确保工具元信息必须正确初始化 214 | 215 | * **工具注册函数(`SingleIpParser`)** 216 | 217 | * 创建 `IpApiService` 服务实例用于 `IP` 定位查询 218 | 219 | * 返回两个值: 220 | 221 | * 预定义的 `singleIpParserTool` 元信息对象 222 | * 工具处理函数 223 | 224 | * **工具处理函数** 225 | 226 | * **参数验证与解析** 227 | 228 | * 调用 `protocol.VerifyAndUnmarshal` 验证请求参数有效性 229 | * 将原始参数反序列化到 `ipRequest` 结构体 230 | 231 | * **服务调用** 232 | 233 | * 使用 `ipApiService.GetLocation` 获取 `IP` 地理位置信息 234 | 235 | * **结果处理** 236 | 237 | * 将服务响应结果序列化为 `JSON` 字符串并包装为 `protocol.CallToolResult` 结构体返回 238 | 239 | # 🚀 服务器的创建与启动 240 | 241 | 代码位于 `server` 包中,用于初始化服务并启动服务端,具体实现如下: 242 | 243 | ```go 244 | package server 245 | 246 | import ( 247 | "github.com/ThinkInAIXYZ/go-mcp/server" 248 | "github.com/ThinkInAIXYZ/go-mcp/transport" 249 | "github.com/chenmingyong0423/mcp-ip-geo/internal/tools" 250 | ) 251 | 252 | func Run(address string) error { 253 | var err error 254 | 255 | var ts transport.ServerTransport 256 | if address == "" { 257 | ts = transport.NewStdioServerTransport() 258 | } else { 259 | ts, err = transport.NewSSEServerTransport(address) 260 | if err != nil { 261 | return err 262 | } 263 | } 264 | 265 | s, err := server.NewServer(ts) 266 | if err != nil { 267 | return err 268 | } 269 | 270 | toolFuncList := tools.GetToolFuncList() 271 | for _, tool := range toolFuncList { 272 | s.RegisterTool(tool()) 273 | } 274 | 275 | return s.Run() 276 | } 277 | 278 | ``` 279 | 280 | **代码解释:** 281 | 282 | * **传输层初始化** 283 | 284 | * 根据 `address` 参数判断运行模式: 285 | 286 | * **空地址模式**:使用 `NewStdioServerTransport` 创建标准输入输出传输,适用于命令行工具等场景。 287 | * **指定地址模式**:使用 `NewSSEServerTransport` 创建 `SSE` (`Server-Sent Events`) 传输,适用于 `HTTP` 长连接服务。 288 | 289 | * **服务实例化** 290 | 291 | * 使用 `server.NewServer` 方法创建服务实例,注入配置好的传输层对象 `ts`。 292 | 293 | * **工具注册** 294 | 295 | * 调用 `tools.GetToolFuncList` 获取所有预定义的工具函数列表。 296 | 297 | * 遍历工具列表,通过 `s.RegisterTool(tool())` 注册每个工具: 298 | 299 | * `tool()` 执行后返回元信息 `*protocol.Tool` 和处理函数 `ToolHandlerFunc`。 300 | 301 | * **服务启动** 302 | 303 | * 调用 `s.Run()` 启动服务,开始监听请求。 304 | 305 | # 🧩 主程序入口实现 306 | 307 | 代码位于 `main` 包中,作为程序启动入口,具体实现如下: 308 | 309 | ```go 310 | package main 311 | 312 | import ( 313 | "flag" 314 | "github.com/chenmingyong0423/mcp-ip-geo/internal/server" 315 | ) 316 | 317 | func main() { 318 | addr := flag.String("address", "", "The host and port to run the sse server") 319 | flag.Parse() 320 | 321 | if err := server.Run(*addr); err != nil { 322 | panic(err) 323 | } 324 | } 325 | 326 | ``` 327 | 328 | **代码解释:** 329 | 330 | * **命令行参数解析** 331 | 332 | * 定义 `address` 参数: 333 | 334 | * 参数名称:`-address` 335 | * 默认值:空字符串 336 | * 描述:指定 `SSE` 服务运行的地址和端口 337 | 338 | * 调用 `flag.Parse()` 解析命令行参数 339 | 340 | * **服务启动** 341 | 342 | * 调用 `server.Run(*addr)` 启动服务 343 | * 将解析后的 `address` 参数值传递给服务启动函数 344 | 345 | # ⚙️ 从源码构建 346 | 347 | 348 | ## 🛠 本地构建 349 | 350 | ### 💻 使用 Go 命令 351 | 352 | [](https://github.com/chenmingyong0423/mcp-ip-geo/blob/main/README-zh_CN.md#%E4%BD%BF%E7%94%A8-go-%E5%91%BD%E4%BB%A4) 353 | 354 | ``` 355 | # 在类 Unix 系统(Linux/macOS)上 356 | go build -o mcp-ip-geo ./cmd/mcp-ip-geo 357 | 358 | # 在 Windows 上 359 | go build -o mcp-ip-geo.exe .\cmd\mcp-ip-geo 360 | ``` 361 | 362 | ### 🐋 使用 Docker 363 | 364 | [](https://github.com/chenmingyong0423/mcp-ip-geo/blob/main/README-zh_CN.md#%E4%BD%BF%E7%94%A8-docker) 365 | 366 | 1. 构建 Docker 镜像: 367 | 368 | ``` 369 | docker build -t mcp-ip-geo-server . 370 | ``` 371 | 372 | 1. 运行容器: 373 | 374 | ``` 375 | docker run -d --name mcp-ip-geo-server -p 8000:8000 mcp-ip-geo-server 376 | ``` 377 | 378 | ## 📦 安装预编译版本 379 | 380 | [](https://github.com/chenmingyong0423/mcp-ip-geo/blob/main/README-zh_CN.md#%E5%AE%89%E8%A3%85%E9%A2%84%E7%BC%96%E8%AF%91%E7%89%88%E6%9C%AC) 381 | 382 | 使用 `Go` 安装最新版本的服务: 383 | 384 | ``` 385 | go install github.com/chenmingyong0423/mcp-ip-geo/cmd/mcp-ip-geo@latest 386 | ``` 387 | 388 | # 🧩 MCP 集成 389 | 390 | [](https://github.com/chenmingyong0423/mcp-ip-geo/blob/main/README-zh_CN.md#mcp-%E9%9B%86%E6%88%90) 391 | 392 | 你可以通过以下两种方式集成 `mcp-ip-geo` 服务: 393 | 394 | ## 🖥 可执行文件集成(本地运行) 395 | 396 | [](https://github.com/chenmingyong0423/mcp-ip-geo/blob/main/README-zh_CN.md#-%E5%8F%AF%E6%89%A7%E8%A1%8C%E6%96%87%E4%BB%B6%E9%9B%86%E6%88%90%E6%9C%AC%E5%9C%B0%E8%BF%90%E8%A1%8C) 397 | 398 | ``` 399 | { 400 | "mcpServers": { 401 | "mcp-ip-geo": { 402 | "command": "/path/to/mcp-ip-geo" 403 | } 404 | } 405 | } 406 | ``` 407 | 408 | ## 🌐 HTTP 接口集成(连接到已运行的服务) 409 | 410 | [](https://github.com/chenmingyong0423/mcp-ip-geo/blob/main/README-zh_CN.md#-http-%E6%8E%A5%E5%8F%A3%E9%9B%86%E6%88%90%E8%BF%9E%E6%8E%A5%E5%88%B0%E5%B7%B2%E8%BF%90%E8%A1%8C%E7%9A%84%E6%9C%8D%E5%8A%A1) 411 | 412 | ``` 413 | { 414 | "mcpServers": { 415 | "mcp-ip-geo": { 416 | "url": "http://host:port/sse" 417 | } 418 | } 419 | } 420 | ``` 421 | 422 | # 👀 效果演示 423 | 424 | ![企业微信截图_17455550209491](https://github.com/user-attachments/assets/d2f7f0ac-2a97-43e2-9dd0-341bd209c234) 425 | 426 | 427 | # ✅ 小结 428 | 429 | 本文将详细介绍 `mcp-ip-geo` —— 一个用于查询 `IP` 信息的 `MCP` 服务器的实现细节。该服务器目前支持两种数据传输方式:`stdio` 和 `SSE(Server-Sent Events)`。未来还计划支持 `Streamable HTTP` 传输方式,并持续扩展更多实用的工具(`tools`)模块。 430 | -------------------------------------------------------------------------------- /tutorials/一文掌握 MCP 上下文协议:从理论到实践/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenmingyong0423/mcp-tutorials/1f79733f6c34c81500983e19aa5a739b6734b0d8/tutorials/一文掌握 MCP 上下文协议:从理论到实践/images/1.png -------------------------------------------------------------------------------- /tutorials/一文掌握 MCP 上下文协议:从理论到实践/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenmingyong0423/mcp-tutorials/1f79733f6c34c81500983e19aa5a739b6734b0d8/tutorials/一文掌握 MCP 上下文协议:从理论到实践/images/2.png -------------------------------------------------------------------------------- /tutorials/一文掌握 MCP 上下文协议:从理论到实践/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenmingyong0423/mcp-tutorials/1f79733f6c34c81500983e19aa5a739b6734b0d8/tutorials/一文掌握 MCP 上下文协议:从理论到实践/images/3.png -------------------------------------------------------------------------------- /tutorials/一文掌握 MCP 上下文协议:从理论到实践/一文掌握 MCP 上下文协议:从理论到实践.md: -------------------------------------------------------------------------------- 1 | # MCP 2 | 3 | 模型上下文协议(`Model Context Protocol`,简称 `MCP`)是一种开放标准,旨在标准化大型语言模型(`LLM`)与外部数据源和工具之间的交互方式。 由 `Anthropic` 于 **2024** 年 **11** 月推出,`MCP` 通过定义统一的接口,使 `AI` 应用能够安全、灵活地访问和操作本地及远程数据资源,提升模型的功能性和可扩展性。 4 | 5 | ![1.png](images/1.png) 6 | 7 | # Transports(传输层) 8 | 9 | 在 `MCP` 协议中,传输层提供了客户端与服务器之间通信的基础,其负责处理消息的发送与接收的底层机制。 10 | 11 | ## 消息格式 12 | 13 | 14 | `MCP` 协议使用 `JSON-RPC 2.0` 作为消息传输格式,包含以下三种类型的 `JSON-RPC` 消息: 15 | 16 | - `Request` 请求: 17 | ```json 18 | { 19 | "jsonrpc": "2.0", 20 | "id": 1, // 请求 ID(数字或字符串) 21 | "method": "string", // 方法名 22 | "params": {} // 可选,参数对象 23 | } 24 | ``` 25 | 26 | - `Response` 响应: 27 | ```json 28 | { 29 | "jsonrpc": "2.0", 30 | "id": 1, // 对应请求的 ID 31 | "result": {}, // 可选,成功结果 32 | "error": { // 可选,错误信息 33 | "code": 123, 34 | "message": "错误描述", 35 | "data": {} // 可选,附加数据 36 | } 37 | } 38 | ``` 39 | 40 | - `Notification`通知: 41 | ```json 42 | { 43 | "jsonrpc": "2.0", 44 | "method": "string", // 通知方法名 45 | "params": {} // 可选,参数对象 46 | } 47 | ``` 48 | 49 | ## 内置传输类型 50 | 51 | 52 | `MCP` 协议内置了两种标准传输方式:**标准输入/输出(stdio)** 和 **Server-Sent Events(SSE)** 。 53 | 54 | ### 标准输入/输出(stdio) 55 | 56 | `stdio` 传输通过 **标准输入输出流** 实现客户端与服务器之间的通信,适用于本地集成与命令行工具。 57 | 58 | 推荐在以下场景使用 `stdio`: 59 | 60 | - 构建命令行工具 61 | 62 | - 本地系统集成 63 | 64 | - 简单进程间通信 65 | 66 | - 与 `shell` 脚本协作 67 | 68 | `Go server` 示例: 69 | 70 | ```go 71 | s := server.NewMCPServer( 72 | "My Server", // Server name 73 | "1.0.0", // Version 74 | ) 75 | 76 | if err := server.ServeStdio(s); err != nil { 77 | log.Fatalf("Server error: %v", err) 78 | } 79 | ``` 80 | 81 | ### **Server-Sent Events (SSE)** 82 | 83 | `SSE` **传输** 通过 `HTTP POST` 请求实现 **客户端到服务器通信**,同时支持 **服务器到客户端流式传输** 。 84 | 85 | 推荐在以下场景使用 `SSE`: 86 | 87 | - 仅需要服务器到客户端的流式通信 88 | 89 | - 运行在受限网络环境 90 | 91 | - 实现简单的推送更新 92 | 93 | `Go server` 示例: 94 | 95 | ```go 96 | s := server.NewMCPServer( 97 | "My Server", // Server name 98 | "1.0.0", // Version 99 | ) 100 | 101 | sseServer := server.NewSSEServer(s) 102 | err := sseServer.Start(":8080") 103 | if err != nil { 104 | panic(err) 105 | } 106 | ``` 107 | 108 | # MCP 的通用架构 109 | 110 | `MCP` 遵循 `CS` 架构(`client-server`),具体包含的组件如下: 111 | 112 | - `Host` 主机:发起连接 `LLM` 的应用程序,例如 `Claude for Desktop` 或其他的 `AI` 应用。 113 | 114 | - `MCP Client` 客户端:运行在主机里的客户端,与 `MCP Server` 服务器保持 **1:1** 连接,负责协议通信。 115 | 116 | - `MCP Server` 服务器:负责向客户端提供 **资源**、**提示** 和 **工具** 的服务器。 117 | 118 | ![2.png](images/2.png) 119 | 120 | # MCP 客户端 121 | 122 | 123 | 客户端可以实现额外的功能来与 `MCP` 服务器进行交互。 124 | 125 | ## Roots 根 126 | 127 | 128 | `Roots` 是`MCP` 协议中的一个概念,用于界定服务器可操作的边界。客户端可以定义 `Roots`,以告知服务器相关的资源信息及其位置。 129 | 130 | `root` 是客户端建议服务器应关注的 `URI`。当 **客户端** 连接到服务器时,它会声明 **服务器** 应处理哪些 `root`。虽然这些 `root` 主要是文件系统路径,但 `root` 也可以是 `HTTP URL`。 131 | 132 | ```txt 133 | # 文件系统路径 134 | file:///home/chenmingyong/workspace 135 | # HTTP URL 136 | https://chenmingyong.cn/ 137 | ``` 138 | 139 | ### 功能 140 | 141 | `Roots` 有以下几个功能: 142 | 143 | - 引导:指示服务器相关资源及其位置。 144 | 145 | - 明确归属:清楚标识哪些资源属于当前工作区。 146 | 147 | - 便于管理:支持同时处理多个不同的资源。 148 | 149 | ### 工作机制 150 | 151 | 当客户端具备 `Roots` 功能时,通常会: 152 | 153 | - 在连接服务器时声明自身的 `Roots` 支持能力 154 | 155 | - 向服务器发送建议的 `Roots` 列表 156 | 157 | - 在 `Roots` 发生变更时(若协议支持)主动通知服务器 158 | 159 | ### 示例 160 | 161 | ```json 162 | { 163 | "roots": [ 164 | { 165 | "uri": "file:///home/chenmingyong/workspace/frontend", 166 | "name": "Frontend Repository" 167 | }, 168 | { 169 | "uri": "https://chenmingyong.cn/", 170 | "name": "API Endpoint" 171 | } 172 | ] 173 | } 174 | ``` 175 | 176 | ## Sampling 采样 177 | 178 | 采样是 `MCP` 协议中一项强大的功能,允许服务器通过客户端向 `LLM` 大模型请求补全结果,从而实现更复杂的代理行为,同时确保安全性与隐私性。 179 | 180 | ### 工作原理 181 | 182 | 采样流程遵循以下步骤: 183 | 184 | - **服务器发送请求**:服务器向客户端发送 `sampling/createMessage` 请求。 185 | 186 | - **客户端审核请求**:客户端收到请求后可以对其审查和修改。 187 | 188 | - **客户端发起采样**:客户端向 `LLM` 发送采样请求。 189 | 190 | - **客户端审核结果**:客户端审核 `LLM` 返回的补全内容。 191 | 192 | - **客户端返回结果**:客户端将最终结果发送回服务器。 193 | 194 | ![3.png](images/3.png) 195 | 196 | 这种 **人机协作(Human-in-the-loop)** 设计,确保用户能控制 `LLM` 看到的内容以及生成的结果,兼顾自动化与安全性。 197 | 198 | ### 消息格式 199 | 200 | 采样请求使用同一的消息结构,示例如下: 201 | 202 | ```json 203 | { 204 | "messages": [ 205 | { 206 | "role": "user" | "assistant", // 消息角色 207 | "content": { 208 | "type": "text" | "image", // 内容类型 209 | 210 | // 文本内容 211 | "text": "string", 212 | 213 | // 图片内容 214 | "data": "string", // Base64 编码 215 | "mimeType": "string" // MIME 类型 216 | 217 | } 218 | 219 | } 220 | ], 221 | "modelPreferences": { // 可选,模型偏好设置 222 | "hints": [ // 模型提示 223 | { 224 | "name": "string" // 建议的模型名称 225 | 226 | } 227 | ], 228 | "costPriority": 0.0, // 降低成本的优先级 (0-1) 229 | "speedPriority": 0.0, // 低延迟的优先级 (0-1) 230 | "intelligencePriority": 0.0 // 模型能力的优先级 (0-1) 231 | }, 232 | "systemPrompt": "string", // 可选,系统提示词 233 | 234 | "includeContext": "none" | "thisServer" | "allServers", // 上下文包含范围 235 | "temperature": 0.0, // 随机性控制 236 | "maxTokens": 100, // 最大生成 Token 数 237 | 238 | "stopSequences": ["string"], // 停止序列 239 | "metadata": {} // 其他元数据 240 | } 241 | ``` 242 | 243 | ### 响应格式 244 | 245 | 客户端返回采样结果的结构如下: 246 | 247 | ```json 248 | { 249 | "model": "string", // 使用的模型名称 250 | "stopReason": "endTurn" | "stopSequence" | "maxTokens" | "string", // 停止原因 251 | 252 | "role": "user" | "assistant", // 消息角色 253 | "content": { 254 | "type": "text" | "image", // 内容类型 255 | "text": "string", // 文本内容 256 | "data": "string", // 图片内容 (Base64) 257 | "mimeType": "string" // MIME 类型 258 | } 259 | } 260 | ``` 261 | 262 | ### 示例请求 263 | 264 | 以下是一个示例采样请求: 265 | 266 | ```json 267 | { 268 | "method": "sampling/createMessage", 269 | "params": { 270 | "messages": [ 271 | { 272 | "role": "user", 273 | "content": { 274 | "type": "text", 275 | "text": "当前目录下有哪些文件?" 276 | 277 | } 278 | } 279 | ], 280 | "systemPrompt": "你是一名文件系统助手。", 281 | "includeContext": "thisServer", 282 | "maxTokens": 100 283 | } 284 | } 285 | ``` 286 | 287 | # MCP 服务器 288 | 289 | 在 `MCP` 协议中,服务器提供了为 `LLM` 大模型添加上下文的基础构件。通过 `Propmts`(提示词)、`Resources`(资源)和 `Tools`(工具)这三种 **原语(Primitives)**, 客户端、服务器与语言模型之间能够实现 **高效且灵活的交互。** 290 | 291 | ## Prompts 提示词 292 | 293 | 294 | **提示词** 允许服务器定义可复用的提示词模板和工作流,客户端可以轻松将这些模板呈现给用户或 `LLM`。 295 | 296 | ### 提示词结构 297 | 298 | 一个提示词的结构定义如下所示: 299 | 300 | ```json 301 | { 302 | "name": "string", // 提示词唯一标识符 303 | "description": "string", // 可选,人类可读的描述 304 | "arguments": [ // 可选参数列表 305 | { 306 | "name": "string", // 参数标识符 307 | "description": "string", // 可选,参数描述 308 | "required": "boolean" // 是否为必填参数 309 | } 310 | ] 311 | } 312 | 313 | ``` 314 | 315 | ### 获取提示词 316 | 317 | 客户端可以通过调用 `prompts/list` 获取可用的提示词列表: 318 | 319 | **请求示例:** 320 | 321 | ```json 322 | { 323 | method: "prompts/list" 324 | } 325 | ``` 326 | 327 | **响应示例:** 328 | 329 | ```json 330 | { 331 | "prompts": [ 332 | { 333 | "name": "analyze-code", // 提示词唯一标识符 334 | "description": "分析代码以发现潜在改进点", // 提示词描述 335 | "arguments": [ 336 | { 337 | "name": "language", // 参数名称 338 | "description": "编程语言", // 参数描述 339 | "required": true // 是否必填 340 | } 341 | ] 342 | } 343 | ] 344 | } 345 | ``` 346 | 347 | ### 获取提示词详情 348 | 349 | 如果客户端要使用提示词,需要通过 `prompts/get` 接口获取提示词详情。 350 | 351 | **请求示例:** 352 | 353 | ```json 354 | { 355 | "method": "prompts/get", 356 | "params": { 357 | "name": "analyze-code", // 要使用的提示词名称 358 | "arguments": { 359 | "language": "go" // 动态参数 360 | } 361 | } 362 | } 363 | ``` 364 | 365 | **响应示例:** 366 | 367 | ```json 368 | { 369 | "description": "分析 Go 代码以发现潜在改进点", // 提示词描述 370 | "messages": [ 371 | { 372 | "role": "user", // 消息发送方 373 | "content": { 374 | "type": "text", // 内容类型 375 | "text": "请分析以下 Go 代码,找出可能的改进空间:\n\n```go\npackage main\n\nimport "fmt"\n\nfunc main() {\n\n fmt.Println("Hello, World!")\n}\n```" 376 | } 377 | } 378 | ] 379 | } 380 | ``` 381 | 382 | ### 动态提示词 383 | 384 | 提示词不仅可以是静态模板,也支持根据参数动态生成内容,甚至可以嵌入 **资源上下文**。 385 | 386 | **提示词定义示例:** 387 | 388 | ```json 389 | { 390 | "name": "analyze-project", // 提示词名称 391 | "description": "分析项目日志与代码", // 描述 392 | "arguments": [ 393 | { 394 | "name": "timeframe", // 参数名称 395 | "description": "要分析的日志时间范围", // 参数描述 396 | "required": true // 是否必填 397 | }, 398 | { 399 | "name": "fileUri", // 参数名称 400 | "description": "待审查的代码文件 URI", // 参数描述 401 | "required": true // 是否必填 402 | } 403 | ] 404 | } 405 | 406 | ``` 407 | 408 | **prompts/get 请求返回示例:** 409 | 410 | ```json 411 | { 412 | "messages": [ 413 | { 414 | "role": "user", 415 | "content": { 416 | "type": "text", 417 | "text": "请分析以下系统日志和代码文件,找出潜在问题:" 418 | } 419 | }, 420 | { 421 | "role": "user", 422 | "content": { 423 | "type": "resource", 424 | "resource": { 425 | "uri": "logs://chenmingyong/recent?timeframe=1h", // 资源 URI 426 | "text": "[2024-03-14 15:32:11] ERROR: network.py:127 连接超时\n[2024-03-14 15:32:15] WARN: 正在重试连接 (第 2/3 次)\n[2024-03-14 15:32:20] ERROR: 达到最大重试次数", 427 | "mimeType": "text/plain" // 资源类型 428 | } 429 | } 430 | }, 431 | { 432 | "role": "user", 433 | "content": { 434 | "type": "resource", 435 | "resource": { 436 | "uri": "file:///path/chenmingyong/code.py", // 代码文件 URI 437 | "text": "def connect_to_service(timeout=30):\n retries = 3\n for attempt in range(retries):\n try:\n return establish_connection(timeout)\n except TimeoutError:\n if attempt == retries - 1:\n raise\n time.sleep(5)\n\ndef establish_connection(timeout):\n # 连接实现\n pass", 438 | "mimeType": "text/x-python" 439 | } 440 | } 441 | } 442 | ] 443 | } 444 | 445 | ``` 446 | 447 | ### 多轮提示工作流 448 | 449 | 提示词还支持定义多轮对话流程,通过预设的多步提示,引导用户完成复杂任务。 450 | 451 | ```TypeScript 452 | const debugWorkflow = { 453 | name: "debug-error", 454 | async getMessages(error: string) { 455 | return [ 456 | { 457 | role: "user", 458 | content: { 459 | type: "text", 460 | text: `我遇到了一个错误:${error}` 461 | } 462 | }, 463 | { 464 | role: "assistant", 465 | content: { 466 | type: "text", 467 | text: "我来帮你分析这个错误。你之前尝试过哪些操作?" 468 | } 469 | }, 470 | { 471 | role: "user", 472 | content: { 473 | type: "text", 474 | text: "我尝试重启服务,但错误依然存在。" 475 | } 476 | } 477 | ]; 478 | } 479 | }; 480 | ``` 481 | 482 | ## Resources 资源 483 | 484 | **Resources**(资源)是 `MCP` 协议中的核心原语之一,服务器通过它可以向客户端提供可读的数据或内容,用作 `LLM` 交互的上下文信息。 485 | 486 | ### 资源 URI 487 | 488 | 每个资源通过 `URI` 进行标识,格式如下: 489 | 490 | ```Markdown 491 | [协议]://[主机]/[路径] 492 | ``` 493 | 494 | 示例: 495 | 496 | ```Markdown 497 | file:///home/chenmingyong/documents/go.pdf 498 | postgres://database/chenmingyong/schema 499 | screen://localhost/chenmingyong/display1 500 | ``` 501 | 502 | 具体的协议(`protocol`)与路径结构由 `MCP` 服务器自行定义,服务器也可以设计自定义的 `URI` 格式。 503 | 504 | ### 资源类型 505 | 506 | 资源内容分为两种类型: 507 | 508 | #### 文本类型 509 | 510 | 包含 `UTF_8` 编码的文本数据,例如: 511 | 512 | - 源代码 513 | 514 | - 配置文件 515 | 516 | - 日志文件 517 | 518 | - `JSON / XML` 数据 519 | 520 | - 普通文本 521 | 522 | #### 二进制资源 523 | 524 | 包含 `Base64` 编码的原始二进制数据,例如: 525 | 526 | - 图片 527 | 528 | - `PDF` 文件 529 | 530 | - 音频 / 视频文件 531 | 532 | - 其他非文本格式 533 | 534 | ### 获取资源列表 535 | 536 | 客户端可以通过调用服务器的 `resources/list` 获取资源列表,每个资源包含以下信息: 537 | 538 | ```JSON 539 | { 540 | "uri": "string", // 资源唯一标识符 541 | "name": "string", // 资源名称(人类可读) 542 | 543 | "description": "string", // 可选,资源描述 544 | "mimeType": "string" // 可选,资源 MIME 类型 545 | } 546 | ``` 547 | 548 | 对于动态资源,服务器可以通过 `URI` 模板方式暴露资源,客户端可根据模板构建有效的资源 `URI`: 549 | 550 | ```JSON 551 | { 552 | "uriTemplate": "string", // 符合 RFC 6570 的 URI 模板 553 | "name": "string", // 模板名称(人类可读) 554 | 555 | "description": "string", // 可选,模板描述 556 | "mimeType": "string" // 可选,匹配资源的 MIME 类型 557 | } 558 | ``` 559 | 560 | ### 获取资源详情 561 | 562 | 客户端可以通过 `resources/read` 接口读取资源内容,只需传入资源的 `URI`。 563 | 564 | 服务器会返回资源内容列表: 565 | 566 | ```JSON 567 | { 568 | contents: [ 569 | { 570 | uri: string; // 资源唯一标识符 571 | mimeType?: string; // 可选,资源 MIME 类型 572 | 573 | // 以下二选一 574 | text?: string; // 文本资源 575 | blob?: string; // 二进制资源 576 | } 577 | ] 578 | } 579 | ``` 580 | 581 | ### 资源更新 582 | 583 | `MCP` 支持通过两种方式通知资源变更。 584 | 585 | #### 列表变更 586 | 587 | 当可用资源列表发生变更时,服务器会通过 `notifications/resources/list_changed` 通知客户端。 588 | 589 | #### 内容变更 590 | 591 | 客户端可以订阅特定资源的更新流程如下: 592 | 593 | - 客户端发送 `resources/subscribe` 请求,指定资源 `URI` 594 | 595 | - 资源发生变更时,服务器通过 `notifications/resources/updated` 通知客户端 596 | 597 | - 客户端可通过 `resources/read` 获取最新内容 598 | 599 | - 客户端可通过 `resources/unsubscribe` 取消订阅 600 | 601 | ## Tools 工具 602 | 603 | 604 | `Tools`(工具) 是 `MCP` 协议中的一项关键原语,服务器可通过它向客户端暴露可执行功能,供 `LLM` 使用(通常需要用户批准,确保人类参与决策)。`Tools` 的核心概念包括: 605 | 606 | - 发现(**Discovery**):客户端可通过 `tools/list` 接口获取可用工具列表。 607 | 608 | - 调用(**Invocation**):客户端可通过 `tools/call`接口发起工具调用请求,由服务器执行具体操作并返回结果。 609 | 610 | - 灵活性(**Flexibility**):工具既可以是简单的计算函数,也可以是复杂的 `API` 集成。 611 | 612 | ### 工具结构定义 613 | 614 | 每个工具的结构定义如下所示: 615 | 616 | ```JSON 617 | { 618 | "name": "string", // 工具唯一标识符 619 | "description": "string", // 可选,工具描述 620 | 621 | "inputSchema": { // 工具参数的 JSON Schema 622 | "type": "object", 623 | 624 | "properties": { ... } // 工具参数定义 625 | } 626 | } 627 | ``` 628 | 629 | ### 工具实现示例 630 | 631 | - 与本地系统交互的工具 632 | 633 | ```JavaScript 634 | { 635 | "name": "execute_command", // 工具名称 636 | "description": "执行 shell 命令", // 描述 637 | 638 | "inputSchema": { 639 | "type": "object", 640 | 641 | "properties": { 642 | "command": { "type": "string" }, // 命令名称 643 | 644 | "args": { 645 | "type": "array", 646 | 647 | "items": { "type": "string" } // 命令参数 648 | } 649 | } 650 | } 651 | } 652 | 653 | ``` 654 | 655 | - 外部 `API` 集成类工具 656 | 657 | ```JSON 658 | { 659 | "name": "github_create_issue", // 工具名称 660 | "description": "在 GitHub 创建 Issue", // 描述 661 | 662 | "inputSchema": { 663 | "type": "object", 664 | 665 | "properties": { 666 | "title": { "type": "string" }, // Issue 标题 667 | 668 | "body": { "type": "string" }, // Issue 内容 669 | "labels": { 670 | 671 | "type": "array", 672 | "items": { "type": "string" } // Issue 标签 673 | } 674 | 675 | } 676 | } 677 | } 678 | ``` 679 | 680 | # MCP Server 原语控制层级 681 | 682 | 每个原语的 **控制层级** 可总结如下: 683 | 684 | | 原语 | 控制方 | Description 描述 | Example 示例 | 685 | | --- | --- | --- | --- | 686 | | Prompts | 用户控制 | 由用户选择调用的交互式模板 | 斜杠命令(/command)、菜单选项 | 687 | | Resources | 应用控制 | 由客户端附加并管理的上下文数据 | 文件内容,git 历史记录 | 688 | | Tools | 模型控制 | 暴露给 LLM 以便其执行操作的功能接口 | API POST 请求、文件写入 | 689 | 690 | 691 | 692 | # 服务器实现 693 | 694 | 以下是基于 `Go` 语言实现的 `MCP` 服务器代码示例: 695 | 696 | ```Go 697 | package main 698 | 699 | import ( 700 | "context" 701 | "errors" 702 | "fmt" 703 | "os" 704 | 705 | "github.com/mark3labs/mcp-go/mcp" 706 | "github.com/mark3labs/mcp-go/server" 707 | ) 708 | 709 | func main() { 710 | s := server.NewMCPServer( 711 | "Server Demo", 712 | "1.0.0", 713 | ) 714 | 715 | // 添加工具 716 | { 717 | calculatorTool := mcp.NewTool("calculate", 718 | mcp.WithDescription("执行基本的算术运算"), 719 | mcp.WithString("operation", 720 | mcp.Required(), 721 | mcp.Description("要执行的算术运算类型"), 722 | mcp.Enum("add", "subtract", "multiply", "divide"), // 保持英文 723 | ), 724 | mcp.WithNumber("x", 725 | mcp.Required(), 726 | mcp.Description("第一个数字"), 727 | ), 728 | mcp.WithNumber("y", 729 | mcp.Required(), 730 | mcp.Description("第二个数字"), 731 | ), 732 | ) 733 | 734 | s.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 735 | op := request.Params.Arguments["operation"].(string) 736 | x := request.Params.Arguments["x"].(float64) 737 | y := request.Params.Arguments["y"].(float64) 738 | 739 | var result float64 740 | switch op { 741 | case "add": 742 | result = x + y 743 | case "subtract": 744 | result = x - y 745 | case "multiply": 746 | result = x * y 747 | case "divide": 748 | if y == 0 { 749 | return nil, errors.New("不允许除以零") 750 | } 751 | result = x / y 752 | } 753 | 754 | return mcp.FormatNumberResult(result), nil 755 | }) 756 | } 757 | 758 | // 添加资源 759 | { 760 | // 静态资源示例 - 暴露一个 README 文件 761 | resource := mcp.NewResource( 762 | "docs://readme", 763 | "项目说明文档", 764 | mcp.WithResourceDescription("项目的 README 文件"), 765 | mcp.WithMIMEType("text/markdown"), 766 | ) 767 | 768 | // 添加资源及其处理函数 769 | s.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { 770 | content, err := os.ReadFile("README.md") 771 | if err != nil { 772 | return nil, err 773 | } 774 | 775 | return []mcp.ResourceContents{ 776 | mcp.TextResourceContents{ 777 | URI: "docs://readme", 778 | MIMEType: "text/markdown", 779 | Text: string(content), 780 | }, 781 | }, nil 782 | }) 783 | } 784 | 785 | // 添加提示词 786 | { 787 | // 简单问候提示 788 | s.AddPrompt(mcp.NewPrompt("greeting", 789 | mcp.WithPromptDescription("一个友好的问候提示"), 790 | mcp.WithArgument("name", 791 | mcp.ArgumentDescription("要问候的人的名字"), 792 | ), 793 | ), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { 794 | name := request.Params.Arguments["name"] 795 | if name == "" { 796 | name = "朋友" 797 | } 798 | 799 | return mcp.NewGetPromptResult( 800 | "友好的问候", 801 | []mcp.PromptMessage{ 802 | mcp.NewPromptMessage( 803 | mcp.RoleAssistant, 804 | mcp.NewTextContent(fmt.Sprintf("你好,%s!今天有什么可以帮您的吗?", name)), 805 | ), 806 | }, 807 | ), nil 808 | }) 809 | } 810 | 811 | // 启动基于 stdio 传输类型的服务 812 | if err := server.ServeStdio(s); err != nil { 813 | fmt.Printf("Server error: %v\n", err) 814 | } 815 | } 816 | 817 | ``` 818 | 819 | 上述代码示例演示了如何创建一个 `MCP Server`,并展示了添加工具、资源和提示词的方法。 820 | 821 | 需要注意的是,由于 `mcp-go` 库当前尚未支持 `Sampling`(采样)功能,示例中未包含该功能的相关用法。 822 | 823 | 824 | # 客户端实现 825 | 826 | 基于上面定义的服务器,以下是基于 `Go` 语言实现的 `MCP` 客户端代码示例: 827 | 828 | ```Go 829 | package main 830 | 831 | import ( 832 | "context" 833 | "fmt" 834 | "time" 835 | 836 | "github.com/mark3labs/mcp-go/client" 837 | "github.com/mark3labs/mcp-go/mcp" 838 | ) 839 | 840 | func main() { 841 | 842 | // 创建一个基于 stdio 的MCP客户端 843 | mcpClient, err := client.NewStdioMCPClient( 844 | "./client/server", 845 | []string{}, 846 | ) 847 | if err != nil { 848 | panic(err) 849 | } 850 | defer mcpClient.Close() 851 | 852 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 853 | defer cancel() 854 | 855 | fmt.Println("初始化 mcp 客户端...") 856 | initRequest := mcp.InitializeRequest{} 857 | initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION 858 | initRequest.Params.ClientInfo = mcp.Implementation{ 859 | Name: "Client Demo", 860 | Version: "1.0.0", 861 | } 862 | 863 | // 初始化MCP客户端并连接到服务器 864 | initResult, err := mcpClient.Initialize(ctx, initRequest) 865 | if err != nil { 866 | panic(err) 867 | } 868 | fmt.Printf( 869 | "\n初始化成功,服务器信息: %s %s\n\n", 870 | initResult.ServerInfo.Name, 871 | initResult.ServerInfo.Version, 872 | ) 873 | 874 | // 从服务器获取提示词列表 875 | fmt.Println("提示词列表:") 876 | promptsRequest := mcp.ListPromptsRequest{} 877 | prompts, err := mcpClient.ListPrompts(ctx, promptsRequest) 878 | if err != nil { 879 | panic(err) 880 | } 881 | for _, prompt := range prompts.Prompts { 882 | fmt.Printf("- %s: %s\n", prompt.Name, prompt.Description) 883 | fmt.Println("参数:", prompt.Arguments) 884 | } 885 | 886 | // 从服务器获取资源列表 887 | fmt.Println() 888 | fmt.Println("资源列表:") 889 | resourcesRequest := mcp.ListResourcesRequest{} 890 | resources, err := mcpClient.ListResources(ctx, resourcesRequest) 891 | if err != nil { 892 | panic(err) 893 | } 894 | for _, resource := range resources.Resources { 895 | fmt.Printf("- uri: %s, name: %s, description: %s, MIME类型: %s\n", resource.URI, resource.Name, resource.Description, resource.MIMEType) 896 | } 897 | 898 | // 从服务器获取工具列表 899 | fmt.Println() 900 | fmt.Println("可用工具列表:") 901 | toolsRequest := mcp.ListToolsRequest{} 902 | tools, err := mcpClient.ListTools(ctx, toolsRequest) 903 | if err != nil { 904 | panic(err) 905 | } 906 | 907 | for _, tool := range tools.Tools { 908 | fmt.Printf("- %s: %s\n", tool.Name, tool.Description) 909 | fmt.Println("参数:", tool.InputSchema.Properties) 910 | } 911 | fmt.Println() 912 | 913 | // 调用工具 914 | fmt.Println("调用工具: calculate") 915 | toolRequest := mcp.CallToolRequest{ 916 | Request: mcp.Request{ 917 | Method: "tools/call", 918 | }, 919 | } 920 | toolRequest.Params.Name = "calculate" 921 | toolRequest.Params.Arguments = map[string]any{ 922 | "operation": "add", 923 | "x": 1, 924 | "y": 1, 925 | } 926 | // Call the tool 927 | result, err := mcpClient.CallTool(ctx, toolRequest) 928 | if err != nil { 929 | panic(err) 930 | } 931 | fmt.Println("调用工具结果:", result.Content[0].(mcp.TextContent).Text) 932 | } 933 | 934 | ``` 935 | 936 | 运行上述代码的结果如下所示: 937 | 938 | ```TXT 939 | 初始化 mcp 客户端... 940 | 941 | 初始化成功,服务器信息: Server Demo 1.0.0 942 | 943 | 提示词列表: 944 | - greeting: 一个友好的问候提示 945 | 参数: [{name 要问候的人的名字 false}] 946 | 947 | 资源列表: 948 | - uri: docs://readme, name: 项目说明文档, description: 项目的 README 文件, MIME类型: text/markdown 949 | 950 | 可用工具列表: 951 | - calculate: 执行基本的算术运算 952 | 参数: map[operation:map[description:要执行的算术运算类型 enum:[add subtract multiply divide] type:string] x:map[description:第一个数字 type:number] y:map[description:第二个数字 type:number]] 953 | 954 | 调用工具: calculate 955 | 调用工具结果: 2.00 956 | 957 | ``` 958 | 959 | 提示词、资源以及工具列表和之前定义 `MCP Server` 时所设置的数据一致。 960 | 961 | 962 | # 小结 963 | 964 | 本文介绍了 **模型上下文协议(Model Context Protocol,MCP)**,一种用于规范大型语言模型(`LLM`)与外部数据源及工具之间交互的开放标准。内容涵盖了 `MCP` 协议的整体架构(客户端与服务器的一对一连接模式)、消息传输机制(采用 `JSON-RPC 2.0` 格式)、以及客户端与服务器支持的核心原语,包括: 965 | 966 | - 客户端原语:**roots(根路径)**、**sampling(采样)** 967 | 968 | - 服务器原语:**prompts(提示词)**、**resources(资源)**、**tools(工具)** 969 | 970 | - `Server`原语的控制层级分类 971 | 972 | 最后,本文提供了基于 **Go 语言** 的 `MCP` 客户端与服务器的示例实现,帮助开发者快速理解和应用该协议。 973 | -------------------------------------------------------------------------------- /tutorials/三句话搞定周末出行攻略!我用 AI 生成一日游可视化页面,还能秒上线!/三句话搞定周末出行攻略!我用 AI 生成一日游可视化页面,还能秒上线!.md: -------------------------------------------------------------------------------- 1 | 2 | 前言 3 | == 4 | 5 | 随着 `MCP` 的快速普及和广泛应用,越来越多的工具和服务开始推出自身的 `MCP Server`。借助这些服务,用户在与 `AI` 对话时可以调用相应功能,从而赋予模型更强的执行能力和更丰富的交互体验。 6 | 7 | 本文将介绍如何结合 `AI` 与高德地图、`EdgeOne Pages Deploy` 提供的 `MCP Server`,通过三句话即可生成一份可视化的一日游行程页面,并部署到 `EdgeOne` 上以便在线访问。 8 | 9 | 准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。 10 | 11 | 12 | ![image](https://github.com/user-attachments/assets/75166552-b69a-4879-b3aa-4e007e098325) 13 | 14 | 15 | 效果展示 16 | ==== 17 | 18 | 在线访问地址:https://mcp.edgeone.site/share/32wIUaOLr2fnUx3vnB2BU。 19 | 20 | ![image](https://github.com/user-attachments/assets/ba57b52b-1284-452a-bfe2-cf373da2fd66) 21 | 22 | 23 | 24 | AI 生成一日游可视化页面 25 | ============= 26 | 27 | 📐 设计构思 28 | ------- 29 | 30 | 构建一个对话式 `AI` 一日游攻略网页生成的 "助手",用户通过自然语言与 `AI` 聊天,即可获取所在城市或指定地点的轻量一日游推荐行程。"助手" 由 `AI` 聊天大模型、高德 `MCP` 和 `EdgeOne Pages Deploy MCP` 共同驱动,在无需用户编写代码或操作复杂工具的情况下,完成从旅游信息查询、行程规划、网页构建到公开部署的全流程体验。 31 | 32 | 通过与 `AI` 对话,用户可以逐步明确需求,例如 "**我现在在深圳,安排一个轻松的一日游路线,推荐几个小吃店和景点---**"。`AI` 根据语义理解调用高德 `MCP` 提供的位置、景点和美食信息,再综合设计成网页展示所需的 `HTML` 页面结构,最终调用 `EdgeOne Pages Deploy MCP` 进行部署,生成一个公开访问的网页链接,用户可直接使用或转发分享。 33 | 34 | ⚙️ 实现原理 35 | ------- 36 | 37 | ![image](https://github.com/user-attachments/assets/7ed302cd-1b2f-47c2-b553-7d7ba2be37c6) 38 | 39 | 40 | 41 | - **Cursor AI 自然语言解析与上下文管理** 42 | 43 | 用户通过 `Cursor AI` 使用自然语言输入需求,`AI`将自动解析用户位置、偏好(如"一日游"、"情侣"、"想吃当地小吃"等),并在对话中逐步明确用户意图。 44 | 45 | - **高德地图 MCP 信息聚合** 46 | 47 | - `AI` 调用高德地图 `MCP` 获取用户当前位置附近的: 48 | 49 | - 人气景点(根据距离、热度筛选) 50 | 51 | - 餐饮店铺(如本地特色小吃) 52 | 53 | - 开放时间等信息 54 | 55 | - `AI` 根据时间预算、路线合理性,编排一条 **一日游动线** 56 | 57 | - **HTML 页面生成与部署** 58 | 59 | - `AI` 根据行程规划结果,生成标准化 `HTML` 页面,包含: 60 | 61 | - 行程时间表(上午/中午/下午/傍晚) 62 | 63 | - 景点与餐饮介绍(含高德数据字段) 64 | 65 | - 简洁风格的网页结构与样式 66 | 67 | - 最后调用 `EdgeOne Pages Deploy MCP`,将 `HTML` 源码部署至 `EdgeOne Pages`,并返回公开 `URL`,供用户访问、存档或分享。 68 | 69 | 70 | 前置准备 71 | ---- 72 | 73 | ### Cursor 下载 74 | 75 | `Cursor` 是一种全新的智能 `IDE`,通过与 `AI` 的无缝集成而提供支持。除了 `Cursor`,你也可以选择其他支持 `MCP` 功能的 `IDE`。 76 | 77 | ![image](https://github.com/user-attachments/assets/6361e10a-b548-415a-ab36-007453c6a7c9) 78 | 79 | 80 | ### 申请高德地图 API Key 81 | 82 | 本次实践中将使用高德地图的 `MCP Server`,配置包含 `API Key`,因此需要提前完成 `API Key` 的申请。具体步骤可参考:[高德地图 API Key 申请](https://lbs.amap.com/api/mcp-server/create-project-and-key)。 83 | 84 | ![image](https://github.com/user-attachments/assets/ce0e0889-767c-48bf-861f-6635ae23fca7) 85 | 86 | 87 | 在 Cursor 中配置 MCP Server 88 | ----------------------- 89 | 90 | 接下来,我将介绍如何在 `Cursor` 中配置高德地图`MCP Server` 和 `EdgeOne Pages Deploy MCP Server`。 91 | 92 | ### 配置高德地图 MCP Server 93 | 94 | `Amap Maps` 是一个支持任何 `MCP` 协议客户端的服务器,用户可以轻松地通过 `Amap Maps MCP` 服务器来使用各种基于位置的服务。它是高德地图官方 `MCP Server` 。 95 | 96 | - 首先,在 **Cursor Settiongs-MCP** 里,点击 `Add new global MCP server` 按钮跳转到 `MCP Server` 的 `json` 配置文件。 97 | 98 | ![image](https://github.com/user-attachments/assets/7a91631f-14a2-409b-90f9-c50f20fadbc7) 99 | 100 | 101 | - 接下来,将下面的配置复制到 `JSON` 文件里。 102 | 103 | ``` 104 | { 105 | "mcpServers": { 106 | "amap-maps": { 107 | "command": "npx", 108 | "args": [ 109 | "-y", 110 | "@amap/amap-maps-mcp-server" 111 | ], 112 | "env": { 113 | "AMAP_MAPS_API_KEY": "您在高德官网上申请的key" 114 | } 115 | } 116 | } 117 | } 118 | ``` 119 | 120 | `AMAP_MAPS_API_KEY` 的值替换成我们刚才申请的 `API Key`。 121 | 122 | ![image](https://github.com/user-attachments/assets/b2daefa5-5b11-4f10-ae77-8574deaf64b4) 123 | 124 | 125 | 通过以上步骤我们就完成了高德地图的 `MCP Server` 配置。 126 | 127 | ### 配置 EdgeOne Pages Deploy MCP Server 128 | 129 | `EdgeOne Pages` 是基于 `Tencent EdgeOne` 基础设施打造的前端开发和部署平台,专为现代 `Web` 开发设计,帮助开发者快速构建、部署静态站点和无服务器应用。通过集成边缘函数能力,实现高效的内容交付和动态功能扩展,支持全球用户的快速访问。 130 | 131 | `EdgeOne Pages Deploy MCP` 是一项专用服务,能够将 `HTML` 内容快速部署到 `EdgeOne Pages` 并生成公开访问链接。这使您能够立即预览和分享 `AI` 生成的网页内容。 132 | 133 | 配置 `EdgeOne Pages Deploy MCP Server` 和刚才的步骤是一样的**,**以下是对应的 `MCP Server` 配置: 134 | 135 | ``` 136 | { 137 | "mcpServers": { 138 | "edgeone-pages-mcp-server": { 139 | "command": "npx", 140 | "args": ["edgeone-pages-mcp"] 141 | } 142 | } 143 | } 144 | ``` 145 | 146 | #### 技术原理 147 | 148 | `Pages MCP Server` 利用无服务器边缘计算能力和 `KV` 存储,通过 `API` 接收 `HTML` 内容,即可自动生成即时生效的公共访问链接,实现秒级静态页面部署并内置错误处理机制。 149 | 150 | ![image](https://github.com/user-attachments/assets/0a2a6b41-49d5-43e8-95ea-0a277c834b2b) 151 | 152 | 153 | ### MCP Server 配置总览 154 | 155 | 最终的 `mcp.json` 内容为: 156 | 157 | ![image](https://github.com/user-attachments/assets/267cdc06-e52c-4f71-a602-7175340029cb) 158 | 159 | 160 | 在 **Cursor Settiongs-MCP** 里可以看到我们所配置的 `MCP Server` 列表信息。 161 | 162 | ![image](https://github.com/user-attachments/assets/377917aa-7a62-427a-a975-e3d540be8d02) 163 | 164 | 165 | 一日游可视化页面生成 166 | ---------- 167 | 168 | 首先,在 `Chat` 对话模块,选择 `Agent` 模块,将下面的 `prompt` 发给 `AI`: 169 | 170 | > 我现在在深圳,请安排一个轻松的一日游路线,推荐几个小吃店和景点,你可以调用相关的 amap-maps 工具获取相关信息。 171 | 172 | ![image](https://github.com/user-attachments/assets/fc451fad-59ea-46e1-a8dc-55a3c9ce810b) 173 | 174 | 175 | 从上面的图片信息可以发现,`Cursor AI` 通过 `maps_text_search` 工具查询到相关的景点信息,然后规划路线,在规划的时候还会调用其他工具去计算区域之间的距离以及车程时长。下面是 `AI` 规划的完整路线: 176 | 177 | ``` 178 | 深圳一日游完整路线 179 | 上午(9:00-12:00) 180 | 深圳湾公园(开放时间:06:00-23:00) 181 | 地址:滨海大道(近望海路) 182 | 活动:沿海边步行道漫步,欣赏海景,拍照留念 183 | 中午(12:00-14:00) 184 | 前往海上世界(约30分钟车程) 185 | 午餐推荐:金稻园砂锅粥、龙记米粉或蔡澜越南粉 186 | 地址:望海路1128号 187 | 下午(14:00-17:30) 188 | 深圳世界之窗(开放时间:10:00-21:30) 189 | 地址:深南大道9037号 190 | 门票:200元 191 | 活动:参观世界各地著名建筑的微缩景观 192 | 傍晚至晚上(18:00-21:00) 193 | 返回深圳湾公园(约20分钟车程) 194 | 活动:欣赏海湾日落美景 195 | 晚餐:在红树林附近的ICON天际餐厅或香港茗星坊茶餐厅享用晚餐 196 | 这条路线安排紧凑而不匆忙,让您能够体验深圳的自然景观和人文景点,同时品尝到当地特色美食。全程交通便利,景点之间的距离适中,非常适合轻松的一日游。 197 | ``` 198 | 199 | 接下来,我们请求 `AI` 根据路线生成可视化的 `HTML` 页面,下面是 `prompt`: 200 | 201 | > 请根据完整路线,生成一个好看的的 HTML 页面,然后将源码发给我,如果我确定没有问题,我会请求部署该页面。 202 | 203 | ![image](https://github.com/user-attachments/assets/38d8bfa0-039e-4487-b299-ac2bc66213fe) 204 | 205 | 206 | 最后,请求 `AI` 将 `html` 页面部署到 `EdgeOne Pages` 上。 207 | 208 | ![image](https://github.com/user-attachments/assets/e6c81e86-dbc6-4741-b031-dd973db4651a) 209 | 210 | 211 | 部署成功后,`AI` 将会发送访问链接给我们: https://mcp.edgeone.site/share/32wIUaOLr2fnUx3vnB2BU,下面是页面显示效果: 212 | 213 | ![image](https://github.com/user-attachments/assets/d419f178-246d-46b7-9d60-928de74767da) 214 | 215 | 216 | HTML 源码 217 | ======= 218 | 219 | `AI` 根据一日游规划生成的 `HTML` 源码如下: 220 | 221 | ``` 222 | 223 | 224 | 225 | 226 | 227 | 深圳一日游 | 轻松游玩攻略 228 | 229 | 230 | 477 | 478 | 479 | 480 | 504 | 505 | 506 |
507 |
508 |

深圳轻松一日游

509 |

精选景点 - 特色美食 - 完美路线

510 | 查看行程 511 |
512 |
513 | 514 | 515 |
516 |
517 |
518 |
519 |

行程概览

520 |

这条精心设计的路线让您在一天内体验深圳的自然景观和人文景点,同时品尝当地特色美食。全程交通便利,景点之间的距离适中,非常适合轻松的一日游。

521 |
522 |
523 | 524 |
525 |
526 |
527 | 528 |
3个景点
529 |

精选深圳代表性景点

530 |
531 |
532 |
533 |
534 | 535 |
6家美食店
536 |

当地特色小吃与餐厅

537 |
538 |
539 |
540 |
541 | 542 |
12小时
543 |

轻松舒适的游玩时间

544 |
545 |
546 |
547 |
548 | 549 |
40公里
550 |

全程车程总计

551 |
552 |
553 |
554 |
555 |
556 | 557 | 558 |
559 |
560 |

精彩行程

561 |
562 |
563 |
    564 |
  • 565 |
    566 | 567 |
    568 |
    569 |
    570 |

    上午: 深圳湾公园

    571 |

    9:00 - 12:00

    572 |
    573 |
    574 |

    在深圳湾公园开始您的一天。这里有美丽的海景和步行道,可以欣赏深圳湾海岸线的美景,呼吸新鲜空气。

    575 |
    576 | 开放时间: 06:00-23:00 577 | 免费 578 |
    579 |
    580 |
    581 |
  • 582 |
  • 583 |
    584 | 585 |
    586 |
    587 |
    588 |

    午餐: 海上世界美食区

    589 |

    12:00 - 14:00

    590 |
    591 |
    592 |

    在海上世界品尝各式特色美食,推荐尝试金稻园砂锅粥、龙记米粉或蔡澜越南粉。

    593 |
    594 | 车程: 约30分钟 595 | 多种价位选择 596 |
    597 |
    598 |
    599 |
  • 600 |
  • 601 |
    602 | 603 |
    604 |
    605 |
    606 |

    下午: 深圳世界之窗

    607 |

    14:00 - 17:30

    608 |
    609 |
    610 |

    探索深圳世界之窗,这里汇集了世界各地的著名建筑微缩景观,非常适合休闲观光和拍照。

    611 |
    612 | 开放时间: 10:00-21:30 613 | 门票: 200元 614 |
    615 |
    616 |
    617 |
  • 618 |
  • 619 |
    620 | 621 |
    622 |
    623 |
    624 |

    傍晚及晚餐: 深圳湾日落与晚餐

    625 |

    18:00 - 21:00

    626 |
    627 |
    628 |

    返回深圳湾公园欣赏海湾日落美景,随后在红树林附近的ICON天际餐厅或香港茗星坊茶餐厅享用晚餐。

    629 |
    630 | 车程: 约20分钟 631 | 完美结束 632 |
    633 |
    634 |
    635 |
  • 636 |
637 |
638 |
639 |
640 |
641 | 642 | 643 |
644 |
645 |

景点介绍

646 |
647 |
648 |
649 | 深圳湾公园 650 |
651 |
深圳湾公园
652 |

深圳湾公园是一个沿海岸线的城市公园,拥有美丽的海景和步行道,是深圳市民休闲放松的好去处。公园沿线可欣赏深圳湾、香港新界的景色,特别是傍晚时分的日落景色格外迷人。

653 |
654 |
655 | 滨海大道 656 |
657 | 开放时间: 06:00-23:00 658 |
659 |
660 |
661 |
662 | 663 |
664 |
665 | 海上世界 666 |
667 |
海上世界
668 |

海上世界是深圳蛇口的标志性景点,集文化、娱乐、餐饮、购物于一体。这里有多个主题区域,包括游艇中心、特色商店街和美食广场,既可以感受海洋文化,又能品尝各种美食。

669 |
670 |
671 | 望海路1128号 672 |
673 | 全天开放 674 |
675 |
676 |
677 |
678 | 679 |
680 |
681 | 深圳世界之窗 682 |
683 |
深圳世界之窗
684 |

深圳世界之窗是一个汇集了世界著名建筑和景观微缩复制品的主题公园。公园内按不同国家和地区分区,让您足不出户就能领略世界各地的文化特色和建筑风情。

685 |
686 |
687 | 深南大道9037号 688 |
689 | 门票: 200元 690 |
691 |
692 |
693 |
694 |
695 |
696 |
697 | 698 | 699 |
700 |
701 |

美食推荐

702 |
703 |
704 |
705 |

午餐推荐

706 |
707 |
708 |

金稻园砂锅粥

709 |

位于海上世界C区101,这里的砂锅粥香浓可口,食材新鲜,是当地人气美食。

710 |

地址: 望海路海上世界C区101

711 |
712 |
713 |

龙记米粉

714 |

位于海上世界地铁站A口附近,是深圳很有名的特色米粉,口感劲道,汤底鲜美。

715 |

地址: 海上世界地铁站A口步行90米

716 |
717 |
718 |

蔡澜越南粉

719 |

位于海上世界船前广场,由著名美食家蔡澜推荐,提供正宗的越南风味米粉。

720 |

地址: 招商街道水湾社区海上世界船前广场2栋103

721 |
722 |
723 |
724 | 725 |
726 |
727 |

晚餐推荐

728 |
729 |
730 |

ICON天际餐厅

731 |

位于尚美-红树湾1号,可以欣赏到海湾美景,提供精致的中西融合料理,环境优雅。

732 |

地址: 尚美-红树湾1号

733 |
734 |
735 |

香港茗星坊茶餐厅

736 |

位于沙嘴中心街,提供正宗的港式美食,如菠萝包、奶茶、鸳鸯饭等经典港式料理。

737 |

地址: 沙嘴中心街7号

738 |
739 |
740 |

潮味海鲜餐厅

741 |

位于福荣路,专注于提供新鲜的海鲜料理,以粤菜和潮汕风味为主,价格适中。

742 |

地址: 沙头街道福荣路沙嘴村三坊32-33号

743 |
744 |
745 |
746 |
747 |
748 |
749 | 750 | 751 |
752 |
753 |

旅行贴士

754 |
755 |
756 |
757 |
交通出行
758 |
    759 |
  • 深圳公共交通发达,可以使用深圳通卡或手机支付搭乘公交、地铁
  • 760 |
  • 也可以使用滴滴出行、高德打车等APP叫车前往各景点
  • 761 |
  • 世界之窗附近地铁站为"世界之窗站",十分便利
  • 762 |
  • 深圳湾公园附近地铁站为"红树湾站",可步行前往
  • 763 |
  • 海上世界有专门的"海上世界站",交通便利
  • 764 |
765 |
766 |
767 |
768 |
769 |
游玩建议
770 |
    771 |
  • 深圳天气温暖,建议携带防晒用品和遮阳伞
  • 772 |
  • 世界之窗面积较大,建议穿着舒适的鞋子
  • 773 |
  • 深圳湾公园傍晚人较多,建议提前到达找到好位置欣赏日落
  • 774 |
  • 世界之窗门票可以提前在官网或旅游平台购买,避免排队
  • 775 |
  • 建议携带充足的饮用水,特别是在夏季游玩时
  • 776 |
  • 海上世界美食种类丰富,可以少量多尝试不同美食
  • 777 |
778 |
779 |
780 |
781 |
782 |
783 | 784 | 785 | 793 | 794 | 795 | 796 | 797 | ``` 798 | 799 | 小结 800 | == 801 | 802 | 在本次实践中,我们不仅体验了 `AI` 通过 `MCP` 协议连接外部数据服务的能力,还深入探索了 EdgeOne Pages Deploy 在部署环节中的关键作用。作为 **最后一公里** 的部署引擎,`EdgeOne Pages Deploy MCP` 让 `AI` 生成的 `HTML` 页面能够以秒级速度部署至线上,并立即生成公开链接,极大提升了内容的可访问性与传播效率。 803 | 804 | 相比传统的部署流程,`EdgeOne Pages Deploy` 摒弃了复杂的构建配置与发布流程,借助边缘计算和无服务器架构,实现了真正的"所见即所得,生成即上线"。这不仅提升了 `AI` 应用的落地效率,也为开发者、内容创作者、产品设计师等非工程背景用户打开了快速上线创意的全新通道。 805 | -------------------------------------------------------------------------------- /tutorials/用 Go 语言轻松构建 MCP 客户端与服务器/用 Go 语言轻松构建 MCP 客户端与服务器.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 模型上下文协议(`Model Context Protocol`,简称 `MCP`)是一种开放标准,旨在标准化大型语言模型(`LLM`)与外部数据源和工具之间的交互方式。随着 `MCP` 越来越受欢迎,`Go MCP` 库应运而生。本文将介绍如何在 `Go` 语言里面轻松构建 `MCP` 客户端和服务器。 3 | 4 | 准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。 5 | 6 | # mcp-go 7 | 要构建 `MCP` 客户端和服务器,我们需要使用 `mcp-go` 库。 8 | 9 | `mcp-go` 是 `Go` 语言实现的 `Model Context Protocol`(`MCP`)库,通过这个库可以实现 `LLM` 应用与外部数据源和工具之间的无缝集成。 10 | 11 | ## 主要特点 12 | 13 | - 快速:高级接口意味着更少的代码和更快的开发速度 14 | 15 | - 简单:使用极少的样板代码构建 `MCP` 服务器 16 | 17 | - 完整:`MCP Go` 旨在提供 `MCP` 核心规范的完整实现 18 | 19 | 20 | # 安装 MCP 库 21 | 22 | 在 `Go` 项目根目录下,执行以下命令: 23 | 24 | ```bash 25 | go get github.com/mark3labs/mcp-go 26 | ``` 27 | 28 | # 构建 MCP 服务器 29 | 接下来,我们使用 `mcp-go` 提供的 `server` 模块,构建一个通过 `stidio` 方式连接的 `MCP` 服务器。 30 | 31 | ## 创建 server 对象 32 | 33 | ```go 34 | s := server.NewMCPServer( 35 | "Server Demo", 36 | "1.0.0", 37 | ) 38 | ``` 39 | 40 | 创建 `server` 对象时,我们可以指定 **服务器名**,**版本号** 等参数。 41 | 42 | ## 添加工具(tools) 43 | 44 | 以下是一个示例,用于创建并注册一个简单的计算器工具: 45 | 46 | ```go 47 | calculatorTool := mcp.NewTool("calculate", 48 | mcp.WithDescription("执行基本的算术运算"), 49 | mcp.WithString("operation", 50 | mcp.Required(), 51 | mcp.Description("要执行的算术运算类型"), 52 | mcp.Enum("add", "subtract", "multiply", "divide"), // 保持英文 53 | ), 54 | mcp.WithNumber("x", 55 | mcp.Required(), 56 | mcp.Description("第一个数字"), 57 | ), 58 | mcp.WithNumber("y", 59 | mcp.Required(), 60 | mcp.Description("第二个数字"), 61 | ), 62 | ) 63 | 64 | s.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 65 | op := request.Params.Arguments["operation"].(string) 66 | x := request.Params.Arguments["x"].(float64) 67 | y := request.Params.Arguments["y"].(float64) 68 | 69 | var result float64 70 | switch op { 71 | case "add": 72 | result = x + y 73 | case "subtract": 74 | result = x - y 75 | case "multiply": 76 | result = x * y 77 | case "divide": 78 | if y == 0 { 79 | return nil, errors.New("不允许除以零") 80 | } 81 | result = x / y 82 | } 83 | 84 | return mcp.FormatNumberResult(result), nil 85 | }) 86 | ``` 87 | 88 | **添加工具的步骤如下:** 89 | 90 | - 创建工具对象 91 | 使用 `mcp.NewTool` 创建一个工具实例。 92 | - 第一个参数是工具名称(必须),例如 `"calculate"`。 93 | - 其余参数通过函数选项(`functional options`)方式传入,例如: 94 | - `mcp.WithDescription(...)` 添加工具描述; 95 | - `mcp.WithString(...)` 或 `mcp.WithNumber(...)` 定义参数及其规则(如是否必填、参数说明、枚举限制等)。 96 | 97 | - 注册工具到服务器 98 | 通过 `s.AddTool` 方法将工具注册到 `MCP` 服务中。 99 | - 第一个参数是上一步创建的工具对象; 100 | - 第二个参数是该工具的处理函数(handler),用于实现工具的具体逻辑,如参数解析、运算执行、返回结果等。 101 | 102 | 103 | ## 添加资源(Resources) 104 | 105 | 下面的示例展示了如何创建并注册一个静态资源,用于读取并提供 `README.md` 文件的内容。 106 | 107 | ```go 108 | resource := mcp.NewResource( 109 | "docs://readme", 110 | "项目说明文档", 111 | mcp.WithResourceDescription("项目的 README 文件"), 112 | mcp.WithMIMEType("text/markdown"), 113 | ) 114 | 115 | s.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { 116 | content, err := os.ReadFile("README.md") 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | return []mcp.ResourceContents{ 122 | mcp.TextResourceContents{ 123 | URI: "docs://readme", 124 | MIMEType: "text/markdown", 125 | Text: string(content), 126 | }, 127 | }, nil 128 | }) 129 | ``` 130 | 131 | **添加资源的步骤如下:** 132 | 133 | - 创建资源对象 134 | 使用 `mcp.NewResource` 函数创建资源实例。 135 | - 第一个参数为资源 URI,用于标识资源; 136 | - 第二个参数为资源名称; 137 | - 通过函数选项补充更多信息,例如: 138 | - `mcp.WithResourceDescription(...)` 设置资源描述; 139 | - `mcp.WithMIMEType(...)` 指定资源的 MIME 类型。 140 | 141 | - 注册资源处理函数 142 | 使用 `s.AddResource` 将资源对象注册到服务器,并提供一个处理函数: 143 | - 该处理函数会在资源被访问时执行; 144 | - 返回值是资源内容的数组(例如读取本地文件内容并封装为 `TextResourceContents`)。 145 | 146 | 147 | ## 添加提示词(Prompts) 148 | 149 | 以下示例展示了如何创建并添加一个带参数的简单提示词,用于生成个性化的问候语。 150 | 151 | ```go 152 | s.AddPrompt(mcp.NewPrompt("greeting", 153 | mcp.WithPromptDescription("一个友好的问候提示"), 154 | mcp.WithArgument("name", 155 | mcp.ArgumentDescription("要问候的人的名字"), 156 | ), 157 | ), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { 158 | name := request.Params.Arguments["name"] 159 | if name == "" { 160 | name = "朋友" 161 | } 162 | 163 | return mcp.NewGetPromptResult( 164 | "友好的问候", 165 | []mcp.PromptMessage{ 166 | mcp.NewPromptMessage( 167 | mcp.RoleAssistant, 168 | mcp.NewTextContent(fmt.Sprintf("你好,%s!今天有什么可以帮您的吗?", name)), 169 | ), 170 | }, 171 | ), nil 172 | }) 173 | ``` 174 | 175 | --- 176 | 177 | **添加提示词的步骤如下:** 178 | 179 | - 创建提示词对象 180 | 通过 `mcp.NewPrompt` 创建一个提示词定义。 181 | - 第一个参数是提示词名称; 182 | - 可通过 `mcp.WithPromptDescription(...)` 添加描述; 183 | - 使用 `mcp.WithArgument(...)` 定义参数及其说明(如提示词中需要动态插值的内容)。 184 | 185 | - 注册提示词处理函数 186 | 使用 `s.AddPrompt` 将提示词对象注册到服务器,并提供对应的处理逻辑函数: 187 | - 函数接收用户输入参数; 188 | - 返回一个结构化的提示词响应(如构造一个带有用户名字的问候消息)。 189 | 190 | ## 启动基于 `stdio` 传输类型的服务器 191 | 192 | ```go 193 | // 启动基于 stdio 的服务器 194 | if err := server.ServeStdio(s); err != nil { 195 | fmt.Printf("Server error: %v\n", err) 196 | } 197 | ``` 198 | 199 | 使用 `server.ServeStdio` 方法可以启动一个基于标准输入/输出(`stdio`)的 `MCP` 服务器。 200 | 201 | 这种方式适用于本地集成与命令行工具。 202 | 203 | ## 启动基于 `sse`(Server-Sent Events)传输类型的服务器 204 | 205 | 如果需要通过 `HTTP` 的方式提供服务,支持服务端推送数据,可以使用 `SS`E(`Server-Sent Events`)传输模式。 206 | 207 | 208 | ```go 209 | s := server.NewMCPServer( 210 | "My Server", // Server 名称 211 | "1.0.0", // 版本号 212 | ) 213 | 214 | // 创建基于 SSE 的服务器实例 215 | sseServer := server.NewSSEServer(s) 216 | 217 | // 启动服务器,监听指定端口(如 :8080) 218 | err := sseServer.Start(":8080") 219 | if err != nil { 220 | panic(err) 221 | } 222 | 223 | ``` 224 | 225 | 与 `stdio` 不同,`sse` 模式基于 `HTTP` 协议,更适合 `Web` 应用中的长连接场景,支持服务端推送数据。 226 | 227 | ## 完整的 stdio 代码示例 228 | ```go 229 | package main 230 | 231 | import ( 232 | "context" 233 | "errors" 234 | "fmt" 235 | "os" 236 | 237 | "github.com/mark3labs/mcp-go/mcp" 238 | "github.com/mark3labs/mcp-go/server" 239 | ) 240 | 241 | func main() { 242 | s := server.NewMCPServer( 243 | "Server Demo", 244 | "1.0.0", 245 | ) 246 | 247 | // 添加工具 248 | { 249 | calculatorTool := mcp.NewTool("calculate", 250 | mcp.WithDescription("执行基本的算术运算"), 251 | mcp.WithString("operation", 252 | mcp.Required(), 253 | mcp.Description("要执行的算术运算类型"), 254 | mcp.Enum("add", "subtract", "multiply", "divide"), // 保持英文 255 | ), 256 | mcp.WithNumber("x", 257 | mcp.Required(), 258 | mcp.Description("第一个数字"), 259 | ), 260 | mcp.WithNumber("y", 261 | mcp.Required(), 262 | mcp.Description("第二个数字"), 263 | ), 264 | ) 265 | 266 | s.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 267 | op := request.Params.Arguments["operation"].(string) 268 | x := request.Params.Arguments["x"].(float64) 269 | y := request.Params.Arguments["y"].(float64) 270 | 271 | var result float64 272 | switch op { 273 | case "add": 274 | result = x + y 275 | case "subtract": 276 | result = x - y 277 | case "multiply": 278 | result = x * y 279 | case "divide": 280 | if y == 0 { 281 | return nil, errors.New("不允许除以零") 282 | } 283 | result = x / y 284 | } 285 | 286 | return mcp.FormatNumberResult(result), nil 287 | }) 288 | } 289 | 290 | // 添加资源 291 | { 292 | // 静态资源示例 - 暴露一个 README 文件 293 | resource := mcp.NewResource( 294 | "docs://readme", 295 | "项目说明文档", 296 | mcp.WithResourceDescription("项目的 README 文件"), 297 | mcp.WithMIMEType("text/markdown"), 298 | ) 299 | 300 | // 添加资源及其处理函数 301 | s.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { 302 | content, err := os.ReadFile("README.md") 303 | if err != nil { 304 | return nil, err 305 | } 306 | 307 | return []mcp.ResourceContents{ 308 | mcp.TextResourceContents{ 309 | URI: "docs://readme", 310 | MIMEType: "text/markdown", 311 | Text: string(content), 312 | }, 313 | }, nil 314 | }) 315 | } 316 | 317 | // 添加提示词 318 | { 319 | // 简单问候提示 320 | s.AddPrompt(mcp.NewPrompt("greeting", 321 | mcp.WithPromptDescription("一个友好的问候提示"), 322 | mcp.WithArgument("name", 323 | mcp.ArgumentDescription("要问候的人的名字"), 324 | ), 325 | ), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { 326 | name := request.Params.Arguments["name"] 327 | if name == "" { 328 | name = "朋友" 329 | } 330 | 331 | return mcp.NewGetPromptResult( 332 | "友好的问候", 333 | []mcp.PromptMessage{ 334 | mcp.NewPromptMessage( 335 | mcp.RoleAssistant, 336 | mcp.NewTextContent(fmt.Sprintf("你好,%s!今天有什么可以帮您的吗?", name)), 337 | ), 338 | }, 339 | ), nil 340 | }) 341 | } 342 | 343 | // 启动基于 stdio 的服务器 344 | if err := server.ServeStdio(s); err != nil { 345 | fmt.Printf("Server error: %v\n", err) 346 | } 347 | 348 | } 349 | 350 | ``` 351 | 352 | # 构建 MCP 客户端 353 | 354 | 接下来,我们使用 `mcp-go` 提供的 `client` 模块,构建一个通过 `stdio` 方式连接到前面打包好的 `MCP` 服务器的客户端。 355 | 356 | 该客户端将展示以下功能: 357 | 358 | - 初始化客户端并连接服务器 359 | - 获取提示词、资源、工具列表 360 | - 调用远程工具(tool) 361 | 362 | ## 创建 MCP 客户端 363 | 364 | ```go 365 | mcpClient, err := client.NewStdioMCPClient( 366 | "./client/server", // 服务器可执行文件路径 367 | []string{}, // 启动参数(如果有) 368 | ) 369 | if err != nil { 370 | panic(err) 371 | } 372 | defer mcpClient.Close() 373 | ``` 374 | 375 | 通过 `client.NewStdioMCPClient` 方法可以创建一个基于 `stdio` 传输的客户端,并连接到指定的 `MCP` 服务器可执行文件。 376 | 377 | ## 初始化客户端连接 378 | 379 | ```go 380 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 381 | defer cancel() 382 | 383 | initRequest := mcp.InitializeRequest{} 384 | initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION 385 | initRequest.Params.ClientInfo = mcp.Implementation{ 386 | Name: "Client Demo", 387 | Version: "1.0.0", 388 | } 389 | 390 | initResult, err := mcpClient.Initialize(ctx, initRequest) 391 | if err != nil { 392 | panic(err) 393 | } 394 | fmt.Printf("初始化成功,服务器信息: %s %s\n", initResult.ServerInfo.Name, initResult.ServerInfo.Version) 395 | ``` 396 | 397 | 初始化操作通过 `Initialize` 方法完成,需指定协议版本及客户端信息。 398 | 399 | --- 400 | 401 | ## 获取提示词(Prompts)列表 402 | 403 | ```go 404 | promptsRequest := mcp.ListPromptsRequest{} 405 | prompts, err := mcpClient.ListPrompts(ctx, promptsRequest) 406 | if err != nil { 407 | panic(err) 408 | } 409 | for _, prompt := range prompts.Prompts { 410 | fmt.Printf("- %s: %s\n", prompt.Name, prompt.Description) 411 | fmt.Println("参数:", prompt.Arguments) 412 | } 413 | ``` 414 | 415 | 客户端可以使用 `ListPrompts` 获取服务器上定义的所有提示词,包括名称、描述和参数结构。 416 | 417 | ## 获取资源(Resources)列表 418 | 419 | ```go 420 | resourcesRequest := mcp.ListResourcesRequest{} 421 | resources, err := mcpClient.ListResources(ctx, resourcesRequest) 422 | if err != nil { 423 | panic(err) 424 | } 425 | for _, resource := range resources.Resources { 426 | fmt.Printf("- uri: %s, name: %s, description: %s, MIME类型: %s\n", 427 | resource.URI, resource.Name, resource.Description, resource.MIMEType) 428 | } 429 | ``` 430 | 431 | 通过 `ListResources` 方法,客户端可以查看服务器上可用的静态或动态资源信息。 432 | 433 | ## 获取工具(Tools)列表 434 | 435 | ```go 436 | toolsRequest := mcp.ListToolsRequest{} 437 | tools, err := mcpClient.ListTools(ctx, toolsRequest) 438 | if err != nil { 439 | panic(err) 440 | } 441 | for _, tool := range tools.Tools { 442 | fmt.Printf("- %s: %s\n", tool.Name, tool.Description) 443 | fmt.Println("参数:", tool.InputSchema.Properties) 444 | } 445 | ``` 446 | 447 | 通过 `ListTools`,客户端可以获取所有注册的工具信息,方便用户交互式选择或自动生成表单调用。 448 | 449 | ## 调用工具(Tool) 450 | 451 | ```go 452 | toolRequest := mcp.CallToolRequest{ 453 | Request: mcp.Request{ 454 | Method: "tools/call", 455 | }, 456 | } 457 | toolRequest.Params.Name = "calculate" 458 | toolRequest.Params.Arguments = map[string]any{ 459 | "operation": "add", 460 | "x": 1, 461 | "y": 1, 462 | } 463 | 464 | result, err := mcpClient.CallTool(ctx, toolRequest) 465 | if err != nil { 466 | panic(err) 467 | } 468 | fmt.Println("调用工具结果:", result.Content[0].(mcp.TextContent).Text) 469 | ``` 470 | 471 | 通过构造 `CallToolRequest`,客户端可以向 `MCP` 服务器发起工具调用请求,并获取返回的结构化结果。 472 | 473 | 在此示例中,我们调用了服务器端注册的 `calculate` 工具,实现 `1 + 1` 运算。 474 | 475 | ## 完整代码示例 476 | 477 | ```go 478 | package main 479 | 480 | import ( 481 | "context" 482 | "fmt" 483 | "time" 484 | 485 | "github.com/mark3labs/mcp-go/client" 486 | "github.com/mark3labs/mcp-go/mcp" 487 | ) 488 | 489 | func main() { 490 | 491 | // 创建一个基于 stdio 的MCP客户端 492 | mcpClient, err := client.NewStdioMCPClient( 493 | "./client/server", 494 | []string{}, 495 | ) 496 | if err != nil { 497 | panic(err) 498 | } 499 | defer mcpClient.Close() 500 | 501 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 502 | defer cancel() 503 | 504 | fmt.Println("初始化 mcp 客户端...") 505 | initRequest := mcp.InitializeRequest{} 506 | initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION 507 | initRequest.Params.ClientInfo = mcp.Implementation{ 508 | Name: "Client Demo", 509 | Version: "1.0.0", 510 | } 511 | 512 | // 初始化MCP客户端并连接到服务器 513 | initResult, err := mcpClient.Initialize(ctx, initRequest) 514 | if err != nil { 515 | panic(err) 516 | } 517 | fmt.Printf( 518 | "\n初始化成功,服务器信息: %s %s\n\n", 519 | initResult.ServerInfo.Name, 520 | initResult.ServerInfo.Version, 521 | ) 522 | 523 | // 从服务器获取提示词列表 524 | fmt.Println("提示词列表:") 525 | promptsRequest := mcp.ListPromptsRequest{} 526 | prompts, err := mcpClient.ListPrompts(ctx, promptsRequest) 527 | if err != nil { 528 | panic(err) 529 | } 530 | for _, prompt := range prompts.Prompts { 531 | fmt.Printf("- %s: %s\n", prompt.Name, prompt.Description) 532 | fmt.Println("参数:", prompt.Arguments) 533 | } 534 | 535 | // 从服务器获取资源列表 536 | fmt.Println() 537 | fmt.Println("资源列表:") 538 | resourcesRequest := mcp.ListResourcesRequest{} 539 | resources, err := mcpClient.ListResources(ctx, resourcesRequest) 540 | if err != nil { 541 | panic(err) 542 | } 543 | for _, resource := range resources.Resources { 544 | fmt.Printf("- uri: %s, name: %s, description: %s, MIME类型: %s\n", resource.URI, resource.Name, resource.Description, resource.MIMEType) 545 | } 546 | 547 | // 从服务器获取工具列表 548 | fmt.Println() 549 | fmt.Println("可用工具列表:") 550 | toolsRequest := mcp.ListToolsRequest{} 551 | tools, err := mcpClient.ListTools(ctx, toolsRequest) 552 | if err != nil { 553 | panic(err) 554 | } 555 | 556 | for _, tool := range tools.Tools { 557 | fmt.Printf("- %s: %s\n", tool.Name, tool.Description) 558 | fmt.Println("参数:", tool.InputSchema.Properties) 559 | } 560 | fmt.Println() 561 | 562 | // 调用工具 563 | fmt.Println("调用工具: calculate") 564 | toolRequest := mcp.CallToolRequest{ 565 | Request: mcp.Request{ 566 | Method: "tools/call", 567 | }, 568 | } 569 | toolRequest.Params.Name = "calculate" 570 | toolRequest.Params.Arguments = map[string]any{ 571 | "operation": "add", 572 | "x": 1, 573 | "y": 1, 574 | } 575 | // Call the tool 576 | result, err := mcpClient.CallTool(ctx, toolRequest) 577 | if err != nil { 578 | panic(err) 579 | } 580 | fmt.Println("调用工具结果:", result.Content[0].(mcp.TextContent).Text) 581 | } 582 | 583 | ``` 584 | 585 | # 小结 586 | 587 | 本文介绍了如何使用 `mcp-go` 构建一个完整的 `MCP` 应用,包括服务端和客户端两部分。 588 | 589 | - 服务端支持注册工具(Tool)、资源(Resource)和提示词(Prompt),并可通过 `stdio` 或 `sse` 模式对外提供服务; 590 | - 客户端通过 `stdio` 连接服务器,支持初始化、列出服务内容、调用远程工具等操作。 591 | 592 | --------------------------------------------------------------------------------