从零到一掌握MCP
这份文档系统梳理 MCP 协议核心价值与全栈实践,以 Go 为主、辅以 Python/JS 实现,覆盖 stdio/SSE 通信、Tool/Resource/Prompt 三大能力,包含服务端开发、客户端调用、大模型 Agent 联动及抓包调试,实战性强,可直接作为业内 MCP 落地参考。

从零到一掌握MCP

发布时间:2026-04-09 (2026-04-09)

前面我们聊过大模型、Agent、Function Calling这些核心概念,不知道你有没有发现一个问题

最早的大模型只能对话,后来大模型逐渐会调用工具了

前Function Calling时代

在 OpenAI 等官方推出 Function Calling 之前,大家是这样 “逼模型调用工具” 的:

核心思路

  • 后端写死规则:关键词匹配 / 正则 → 判断要不要调工具
  • 或在 Prompt 里强制要求模型输出固定 JSON 格式

典型 Prompt(2022 年风格)

你是一个助手。
如果用户问天气,请严格按以下格式返回,不要加其他文字:
{"action":"get_weather","params":{"city":"北京"}}
否则直接回答问题。

用户问

北京今天天气如何?

模型理想输出

{"action":"get_weather","params":{"city":"北京"}}

后端流程

  1. 用户提问 → 传给 LLM
  2. LLM 输出 JSON(或乱文本)
  3. 后端解析 JSON
    • 成功 → 调用天气 API → 把结果再给 LLM 总结
    • 失败 → 报错或重试

问题(非常明显)

  • 格式极不稳定:经常加自然语言、注释、错别字、格式残缺
  • 容易 “幻觉”:编不存在的 action/params
  • 复杂意图识别很差:“我明天去上海出差要带伞吗?” 经常不触发天气
  • 工具多了 Prompt 巨长

Function Calling时代

最早是由openai推出的标准,也是后来所有厂商的参照原型。

接口形态

curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-3.5-turbo-0613",
    "messages": [
      {"role": "user", "content": "波士顿天气怎么样?"}
    ],
    "functions": [
      {
        "name": "get_current_weather",
        "description": "获取指定城市的当前天气",
        "parameters": {
          "type": "object",
          "properties": {
            "location": {
              "type": "string",
              "description": "城市,如波士顿"
            },
            "unit": {
              "type": "string",
              "enum": ["celsius", "fahrenheit"]
            }
          },
          "required": ["location"]
        }
      }
    ]
  }'

模型返回

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": null,
        "function_call": {
          "name": "get_current_weather",
          "arguments": "{\n  \"location\": \"波士顿\"\n}"
        }
      },
      "finish_reason": "function_call"
    }
  ]
}
  • 定义函数:用 JSON Schema 告诉模型有哪些工具、参数、含义
  • 模型决策
    • 判断是否需要调用
    • 选哪个函数
    • 生成合法参数 JSON
  • 执行调用:后端本地 / 远程执行函数
  • 结果回传:把函数结果放回messages 再请求模型
  • 生成回答:模型用工具结果生成自然语言回复

MCP到底是什么?

不同厂商的大模型,调用工具的方式五花八门,就像“前USB时代”每一家都有自己单独的充电线,让开发者头疼不已。

而MCP的出现,就相当于给大模型世界制定了“USB通用接口”,彻底解决了这个痛点

MCP的全称是Model Context Protocol,也就是模型上下文协议,是Anthropic在2024年11月25日正式推出并开源的一套标准化协议。它不是一个新的大模型,也不是一个工具,而是一套“规则”——用来统一大模型与外部工具、系统之间的交互规范,让不同厂商的大模型、不同类型的工具,都能“听懂彼此的话”。

简单说,MCP的核心目标就一个:让Agent(智能体)不用再挨个适配不同大模型的调用格式,一套接口通吃所有大模型,大大降低开发和适配成本。

MCP通信机制

MCP协议支持两种主要的通信机制:基于标准输入输出stdio的本地通信和基于SSE(Server-SentEvents)的远程通信。这两种机制都使用JSON-RPC 2.0格式进行消息传输,确保了通信的标准化和可扩展性。

  • 本地通信:通过stdio传输数据,适用于在同一台机器上运行的客户端和服务器之间的通信。
  • 远程通信:利用SSE与HTTP结合,实现跨网络的实时数据传输,适用于需要访问远程资源或分布式部署的场景。

MCP Server

MCP Server是 MCP架构中的关键组件,它可以提供3种主要类型的功能:

  • 资源(Resources):类似文件的数据,可以被客户端读取,如API响应或文件内容。
  • 工具(Tools):可以被LLM调用的函数(需要用户批准)。
  • 提示(Prompts):预先编写的模板,帮助用户完成特定任务。

这些功能使MCP server能够为Al应用提供丰富的上下文信息和操作能力,从而增强LLM的实用性和灵活性。

mcp服务端开发

网上的mcp工具,一般都是js和python的比较多

本课程主要以go为主,顺带介绍一下python和js的mcp服务端编写,原理是相通的

go mcp服务端

以go语言为例,可以使用github.com/mark3labs/mcp-go这个库完成mcp适配

stdio本地通信模式

package main

import (
  "context"
  "fmt"
  "github.com/mark3labs/mcp-go/mcp"
  "github.com/mark3labs/mcp-go/server"
)

func main() {
  // 创建一个mcp服务
  s := server.NewMCPServer(
    "我的mcp服务",
    "1.0.0",
    server.WithToolCapabilities(false),
  )

  // 注册工具
  s.AddTool(mcp.NewTool("hello_world",
    mcp.WithDescription("工具描述"),
    mcp.WithString("name",
      mcp.Required(),
      mcp.Description("工具参数的描述"),
    ),
  ), helloHandler)

  err := server.ServeStdio(s)
  if err != nil {
    fmt.Println("stdio监听失败", err)
  }
}

func helloHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  name, err := request.RequireString("name")
  if err != nil {
    return mcp.NewToolResultError(err.Error()), nil
  }

  return mcp.NewToolResultText(fmt.Sprintf("Hello, %s!", name)), nil
}

写完之后,需要编译成可执行文件

配置文件

在cc switch里面加上mcp配置

{
  "command": "E:\\IT\\fengfeng\\test\\mcp_study_go\\mcp_server_stdio.exe",
  "type": "stdio"
}

在apifox里面,直接写 "E:\IT\fengfeng\test\mcp_study_go\mcp_server_stdio.exe" 就可以连接mcp了

stdio只能在本地运行,靠标准输入输出进行进程通信

sse模式

如果你想给你的网站加上scp服务,你本地的agent想调用远端的scp服务

就需要sse模式

package main

import (
  "context"
  "fmt"
  "github.com/mark3labs/mcp-go/mcp"
  "github.com/mark3labs/mcp-go/server"
  "net/http"
)

func main() {
  // 创建一个mcp服务
  s := server.NewMCPServer(
    "我的mcp服务",
    "1.0.0",
    server.WithToolCapabilities(false),
  )

  // 注册工具
  s.AddTool(mcp.NewTool("hello_world",
    mcp.WithDescription("工具描述"),
    mcp.WithString("name",
      mcp.Required(),
      mcp.Description("工具参数的描述"),
    ),
  ), helloHandler)

  sse := server.NewStreamableHTTPServer(s)
  // Start the stdio server
  fmt.Println("mcp server on :8080")
  err := http.ListenAndServe(":8080", sse)
  if err != nil {
    fmt.Println("端口监听失败", err)
  }

}

func helloHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  fmt.Printf("工具调用 %#v\n", request)
  name, err := request.RequireString("name")
  if err != nil {
    return mcp.NewToolResultError(err.Error()), nil
  }

  return mcp.NewToolResultText(fmt.Sprintf("Hello, %s!", name)), nil
}

http模式有两种,一个是sse,一个是stream,需要注意一下

特性 NewStreamableHTTPServer(推荐) NewSSEServer(旧版 / 兼容)
标准 MCP 官方最新标准 早期草案版本
路径 固定/mcp 固定/sse
支持方法 POST + GET(SSE) 仅 GET (SSE)
客户端兼容 所有新客户端(Apifox、Claude Code、Go Client) 仅老客户端

如果是sse模式,连接的路径就是/sse,如果是http模式,连接的路径就是/mcp

配置文件

{
  "headers": {
  },
  "type": "http",
  "url": "http://127.0.0.1:8080/mcp"
}

python mcp服务

安装uv

windows用户,如果还没装过uv的话,可以尝试直接用winget安装

winget install uv

创建uv环境的python项目

uv init weather
cd weather

uv venv
.venv/bin/activate
uv add "mcp[cli]"

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("数学方法")


@mcp.tool()
async def add(a: int, b: int) -> int:
    """两数求和.
        :param b: 需要求和的数
        :param a: 需要求和的数
    """
    return a + b


if __name__ == "__main__":
    mcp.run(transport="stdio")

cc switch里面配置

{
  "command": "python E:\\IT\\fengfeng\\test\\mcp_study\\mcp_study_python\\mcp_stdio.py",
  "type": "stdio"
}

如果要用sse模式的话,这样改

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("数学方法", port=8081)


@mcp.tool()
async def add(a: int, b: int) -> int:
    """两数求和.
        :param b: 需要求和的数
        :param a: 需要求和的数
    """
    return a + b


if __name__ == "__main__":
    mcp.run(transport="streamable-http")

nodejs mcp服务

npm init -y
npm i @modelcontextprotocol/sdk
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
    CallToolRequestSchema,
    ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

// 创建MCP服务器实例
const server = new Server(
    {
        name: "数学工具",
        version: "1.0.0",
        description: "一个用于数学运算的MCP服务器"
    },
    {
        capabilities: {
            tools: {},
        },
    }
);

// 定义工具列表
server.setRequestHandler(ListToolsRequestSchema, async () => {
    return {
        tools: [
            {
                name: "add",
                description: "执行加法运算",
                inputSchema: {
                    type: "object",
                    properties: {
                        a: {
                            type: "number",
                            description: "第一个加数"
                        },
                        b: {
                            type: "number",
                            description: "第二个加数"
                        }
                    },
                    required: ["a", "b"]
                }
            },
        ]
    };
});

// 处理工具调用
server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;

    try {
        let result;

        switch (name) {
            case "add":
                result = args.a + args.b;
                break;
        }

        return {
            content: [
                {
                    type: "text",
                    text: `计算结果: ${result}`
                }
            ]
        };

    } catch (error) {
        return {
            content: [
                {
                    type: "text",
                    text: `错误: ${error.message}`
                }
            ],
            isError: true
        };
    }
});

// 启动服务器
async function main() {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error("Math MCP 服务器已启动");
}

main().catch(console.error);

cc switch里面配置

{
  "command": "node",
  "args": [
    "E:\\IT\\fengfeng\\test\\mcp_study\\mcp_study_js\\mcp_stdio.js"
  ]
}

http的模式有点复杂

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import {
    CallToolRequestSchema,
    ListToolsRequestSchema,
    isInitializeRequest,
} from '@modelcontextprotocol/sdk/types.js';
import express from 'express';

// 为每个会话创建新的 MCP 服务器
function createServer() {
    const server = new Server(
        {
            name: "数学工具",
            version: "1.0.0",
            description: "SSE 模式数学运算服务器"
        },
        { capabilities: { tools: {} } }
    );

    // 注册工具
    server.setRequestHandler(ListToolsRequestSchema, async () => ({
        tools: [
            {
                name: "add",
                description: "两个数字相加",
                inputSchema: {
                    type: "object",
                    properties: {
                        a: { type: "number" },
                        b: { type: "number" }
                    },
                    required: ["a", "b"]
                }
            }
        ]
    }));

    // 处理工具调用
    server.setRequestHandler(CallToolRequestSchema, async (req) => {
        const { name, arguments: args } = req.params;
        try {
            if (name === "add") {
                return {
                    content: [{ type: "text", text: `结果:${args.a + args.b}` }]
                };
            }
            throw new Error(`未知工具:${name}`);
        } catch (e) {
            return {
                content: [{ type: "text", text: `错误:${e.message}` }],
                isError: true
            };
        }
    });

    return server;
}

// 存储 transports 和对应的 servers
const sessions = {};
const app = express();

// 解析 JSON 请求体
app.use(express.json());

// POST 处理 MCP 请求
app.post('/mcp', async (req, res) => {
    console.log('Received MCP request:', req.body);
    try {
        const sessionId = req.headers['mcp-session-id'];
        let session;

        if (sessionId && sessions[sessionId]) {
            // 复用现有 session
            session = sessions[sessionId];
        } else if (!sessionId && isInitializeRequest(req.body)) {
            // 新初始化请求,创建新 session
            const server = createServer();
            const transport = new StreamableHTTPServerTransport({
                sessionIdGenerator: () => crypto.randomUUID(),
                onsessioninitialized: (sid) => {
                    console.log(`Session initialized: ${sid}`);
                    sessions[sid] = session;
                }
            });
            session = { transport, server };
            await server.connect(transport);
            await transport.handleRequest(req, res, req.body);
            return;
        } else {
            // 无效请求
            res.status(400).json({
                jsonrpc: '2.0',
                error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
                id: null
            });
            return;
        }

        await session.transport.handleRequest(req, res, req.body);
    } catch (error) {
        console.error('Error:', error);
        if (!res.headersSent) {
            res.status(500).json({
                jsonrpc: '2.0',
                error: { code: -32603, message: 'Internal server error' },
                id: null
            });
        }
    }
});

// GET 处理 SSE 流
app.get('/mcp', async (req, res) => {
    const sessionId = req.headers['mcp-session-id'];
    if (!sessionId || !sessions[sessionId]) {
        res.status(400).send('Invalid or missing session ID');
        return;
    }
    console.log(`SSE stream for session ${sessionId}`);
    await sessions[sessionId].transport.handleRequest(req, res);
});

// 启动服务器
const PORT = 3000;
app.listen(PORT, () => {
    console.log("✅ MCP SSE 服务器运行成功");
    console.log(`📡 连接地址:http://localhost:${PORT}/mcp`);
});

process.on('SIGINT', async () => {
    console.log('Shutting down...');
    for (const sessionId in sessions) {
        await sessions[sessionId].transport.close();
        delete sessions[sessionId];
    }
    process.exit(0);
});

补充:mcp的resources和prompts

很多基础课程里面没有提到的知识点

这两个东西用好之后,可以让你的mcp变得非常强大

  1. Resource(资源)=给 LLM 读数据的入口
  • LLM 可以直接读取的 “文件 / 数据链接”
  • 不用调用工具,直接read 就能拿内容
  • 用途:读配置、读日志、读接口返回、读数据库结果、读文档
  1. Prompt(提示词模板)=LLM 可以直接调用的话术模板

就是你预先写好的专业提示词,比如:

  • 代码审查模板
  • 翻译模板
  • 客服回复模板
  • 数据分析模板

LLM 不需要你手动粘贴,直接调用这个 Prompt 模板,就能自动套用专业话术。

维度 Tool(工具) Resource(资源) Prompt(提示模板)
核心作用 执行动作、修改状态、产生结果 提供只读数据、补充上下文 提供专业话术 / 任务模板
谁来操作 Agent 调用,MCP Server 执行 Agent 读取,MCP Server 提供数据 Agent 获取模板,交给 LLM 使用
有无副作用 有(可写、可删、可改、可调用外部) 无(只读,不修改任何数据) 无(仅提供文本模板)
是否需要确认 需要(高风险操作) 不需要(安全只读) 不需要(无风险)
LLM 行为 决定 “调用哪个工具 + 传什么参数” 决定 “读取哪个资源” 决定 “使用哪个模板 + 填什么参数”
典型场景 增删改查、执行命令、发送消息、写文件 读文件、读日志、读接口、读数据库、读文档 代码审查、翻译、总结、文案生成、专家角色
通信方法 tools/call resources/read prompts/get
风险等级

go语言 resources、prompts示例

package main

import (
  "context"
  "fmt"
  "github.com/mark3labs/mcp-go/mcp"
  "github.com/mark3labs/mcp-go/server"
  "io"
  "net/http"
  "os"
)

func main() {
  // 创建MCP服务
  s := server.NewMCPServer(
    "MCP三合一服务",
    "1.0.0",
  )

  // ==============================================
  // 1. 注册 Tool:传统工具调用
  // ==============================================
  s.AddTool(mcp.NewTool("hello_world",
    mcp.WithDescription("打招呼工具"),
    mcp.WithString("name", mcp.Required(), mcp.Description("姓名")),
  ), helloHandler)

  // ==============================================
  // 2. 注册 Resource:读取本地文件
  // ==============================================
  s.AddResource(mcp.NewResource(
    "resource://local-file", // 资源URI(固定格式)
    "本地配置文件",                // 资源名称
    mcp.WithResourceDescription("读取本地配置文件"),
  ), readLocalFile)

  // ==============================================
  // 3. 注册 Resource:读取远程接口
  // ==============================================
  s.AddResource(mcp.NewResource(
    "resource://api-ip", // 资源URI
    "IP信息接口",            // 资源名称
    mcp.WithResourceDescription("获取当前出口IP信息"),
  ), readRemoteAPI)

  // ==============================================
  // 4. 注册 Prompt:提示词模板(代码审查)
  // ==============================================
  s.AddPrompt(mcp.NewPrompt(
    "code-review",
    mcp.WithPromptDescription("专业Go代码审查,输出规范、bug、性能、安全"),
    mcp.WithArgument("code",
      mcp.ArgumentDescription("需要审查的代码内容"),
      mcp.RequiredArgument(),
    ),
  ), codeReviewPromptHandler)

  // --- 启动方式 1:stdio 本地模式 ---
  // err := server.ServeStdio(s)

  // --- 启动方式 2:SSE远程模式(推荐) ---
  sseServer := server.NewStreamableHTTPServer(s)
  fmt.Println("MCP server running on :8080")
  err := http.ListenAndServe(":8080", sseServer)

  if err != nil {
    fmt.Println("启动失败:", err)
  }
}

// ==============================================
// Tool 处理函数
// ==============================================
func helloHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  name, _ := request.RequireString("name")
  return mcp.NewToolResultText(fmt.Sprintf("Hello %s!", name)), nil
}

// ==============================================
// Resource 处理函数:读取本地文件
// ==============================================
func readLocalFile(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
  data, err := os.ReadFile("config.txt")
  if err != nil {
    return nil, err
  }
  return []mcp.ResourceContents{
    &mcp.TextResourceContents{
      Text: string(data),
    },
  }, nil
}

// ==============================================
// Resource 处理函数:读取远程接口
// ==============================================
func readRemoteAPI(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
  resp, err := http.Get("https://httpbin.org/ip")
  if err != nil {
    return nil, err
  }
  defer resp.Body.Close()
  body, _ := io.ReadAll(resp.Body)

  return []mcp.ResourceContents{
    &mcp.TextResourceContents{
      Text: string(body),
    },
  }, nil
}

// ==============================================
// Prompt 处理函数:代码审查模板
// ==============================================
func codeReviewPromptHandler(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
  // 从请求参数中获取code参数
  code, ok := request.Params.Arguments["code"]
  if !ok || code == "" {
    return nil, fmt.Errorf("code parameter is required")
  }

  promptText := fmt.Sprintf(`
你是专业的Go工程师,请审查以下代码:
--------------------------------
%s
--------------------------------

请输出:
1. 代码规范问题
2. 潜在Bug
3. 性能优化点
4. 安全风险
`, code)

  return mcp.NewGetPromptResult(
    "专业Go代码审查",
    []mcp.PromptMessage{
      {
        Role:    mcp.RoleAssistant,
        Content: mcp.NewTextContent(promptText),
      },
    },
  ), nil
}

有个问题,大模型更倾向于调用工具,需要一些特定的手段让大模型读资源

补充:mcp客户端开发

开发 MCP 服务端比较简单,因为不涉及大模型交互,只管根据输入返回输出就好了。

如果只是单纯开发 MCP 客户端,也很简单,只需要连接服务端、发起调用、获取结果就行。

package main

import (
  "context"
  "encoding/json"
  "fmt"

  "github.com/mark3labs/mcp-go/client"
  "github.com/mark3labs/mcp-go/mcp"
)

func main() {
  // 1. 创建 MCP 客户端(连接 SSE 服务端)
  mcpClient, err := client.NewStreamableHttpClient(
    "http://127.0.0.1:8080/mcp",
  )
  if err != nil {
    panic(err)
  }

  // 2. 启动客户端
  ctx := context.Background()
  if err := mcpClient.Start(ctx); err != nil {
    panic(err)
  }
  defer mcpClient.Close()

  // 3. 初始化 MCP 协议握手
  initReq := mcp.InitializeRequest{}
  initReq.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
  initReq.Params.ClientInfo = mcp.Implementation{
    Name:    "my-mcp-client",
    Version: "1.0.0",
  }

  _, err = mcpClient.Initialize(ctx, initReq)
  if err != nil {
    panic(fmt.Sprintf("初始化失败: %v", err))
  }
  fmt.Println("✅ MCP 初始化成功")

  // 4. 获取服务端提供的工具列表
  toolsResp, err := mcpClient.ListTools(ctx, mcp.ListToolsRequest{})
  if err != nil {
    panic(fmt.Sprintf("获取工具失败: %v", err))
  }
  fmt.Printf("🔧 可用工具: %d 个\n", len(toolsResp.Tools))
  for _, tool := range toolsResp.Tools {
    fmt.Printf("  - %s: %s\n", tool.Name, tool.Description)
  }
  resourcesResp, err := mcpClient.ListResources(ctx, mcp.ListResourcesRequest{})
  if err != nil {
    panic(fmt.Sprintf("获取资源失败: %v", err))
  }
  fmt.Printf("🔧 可用资源: %d 个\n", len(resourcesResp.Resources))
  for _, resource := range resourcesResp.Resources {
    fmt.Printf("  - %s(%s): %s\n", resource.Name, resource.URI, resource.Description)
  }

  promptsResp, err := mcpClient.ListPrompts(ctx, mcp.ListPromptsRequest{})
  if err != nil {
    panic(fmt.Sprintf("获取提示词失败: %v", err))
  }
  fmt.Printf("🔧 可用提示词: %d 个\n", len(promptsResp.Prompts))
  for _, tool := range promptsResp.Prompts {
    fmt.Printf("  - %s: %s\n", tool.Name, tool.Description)
  }

  // 5. 构造参数,调用工具(hello_world)
  callReq := mcp.CallToolRequest{}
  callReq.Params.Name = "hello_world"
  callReq.Params.Arguments = map[string]any{
    "name": "枫枫网盘",
  }

  // 调用 MCP Server 工具
  result, err := mcpClient.CallTool(ctx, callReq)
  if err != nil {
    panic(fmt.Sprintf("调用失败: %v", err))
  }

  // 6. 输出结果
  fmt.Println("\n📤 工具返回结果:")
  data, _ := json.MarshalIndent(result, "", "  ")
  fmt.Println(string(data))

  resource, err := mcpClient.ReadResource(ctx, mcp.ReadResourceRequest{
    Params: mcp.ReadResourceParams{
      URI: "resource://local-file",
    },
  })
  if err != nil {
    panic(fmt.Sprintf("调用失败: %v", err))
  }
  fmt.Println("\n📤 资源返回结果:")
  data, _ = json.MarshalIndent(resource, "", "  ")
  fmt.Println(string(data))

}

读取资源这里有一个坑点,直接读取会报错,需要在服务端那边改一下

mcp-go库在设计客户端ReadResource的返回处理时,会将 JSON 映射回结构体。如果服务端返回的 JSON 缺失了uri,客户端 SDK 的内部校验逻辑(Validation)会认为这是一个非法或不完整的响应。

func readLocalFile(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {

  data, err := os.ReadFile("config.txt")
  fmt.Println("读取了", string(data))
  if err != nil {
    return nil, err
  }
  return []mcp.ResourceContents{
    &mcp.TextResourceContents{
      // 关键点:显式赋值 URI
      // 很多版本的客户端库会校验返回结果中是否包含原始 URI
      URI:  request.Params.URI,
      Text: string(data),
    },
  }, nil
}

但是,真实场景里 MCP 客户端绝不会单独出现,它一定是和Agent(智能体)+ 大模型 一起工作的。

一旦结合大模型,流程就会复杂很多,因为客户端需要:

  • 调用大模型对话
  • 解析模型输出的工具调用意图
  • 把意图转换成标准 MCP 协议请求
  • 分别处理 Tool / Resource / Prompt 三种能力
  • 把执行结果放回上下文,再交给大模型继续推理

结合大模型使用mcp客户端

package main

import (
  "context"
  "encoding/json"
  "fmt"
  "github.com/mark3labs/mcp-go/client"
  "github.com/mark3labs/mcp-go/mcp"
  openai "github.com/sashabaranov/go-openai"
  "io"
)

func main() {
  // ======================
  // 1. 初始化 MCP 客户端
  // ======================
  client, err := client.NewStreamableHttpClient("http://127.0.0.1:8080/mcp")
  if err != nil {
    panic(err)
  }
  ctx := context.Background()
  _ = client.Start(ctx)
  defer client.Close()

  // 握手
  initReq := mcp.InitializeRequest{}
  initReq.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
  initReq.Params.ClientInfo = mcp.Implementation{Name: "go-agent", Version: "1.0.0"}
  _, _ = client.Initialize(ctx, initReq)

  // ======================
  // 2. 获取 MCP 工具列表
  // ======================
  toolsResp, err := client.ListTools(ctx, mcp.ListToolsRequest{})
  if err != nil {
    fmt.Println(err)
    return
  }
  openAITools := convertToOpenAITools(toolsResp.Tools)

  // ======================
  // 3. 调用大模型
  // ======================
  userQuery := "请帮我对 枫枫知道 打个招呼"

  config := openai.DefaultConfig("") // api-key
  config.BaseURL = "https://open.bigmodel.cn/api/paas/v4/" // 填openai格式的地址

  clientOpenAI := openai.NewClientWithConfig(config)

  stream, err := clientOpenAI.CreateChatCompletionStream(
    context.Background(),
    openai.ChatCompletionRequest{
      Model:    "GLM-4.7",
      Messages: []openai.ChatCompletionMessage{{Role: "user", Content: userQuery}},
      Tools:    openAITools, // 把 MCP 工具喂给大模型
      Stream:   true,
    },
  )
  if err != nil {
    panic(err)
  }
  defer stream.Close()

  // ======================
  // 5. 接收流式输出
  // ======================
  fmt.Println("\n🤖 大模型流式输出:")
  fullText := ""
  var toolCall *openai.ToolCall

  for {
    recv, err := stream.Recv()
    if err == io.EOF {
      fmt.Println(err)
      break
    }
    if err != nil {
      fmt.Println(err)
      panic(err)
    }

    delta := recv.Choices[0].Delta
    if delta.Content != "" {
      fullText += delta.Content
      fmt.Print(delta.Content) // 实时打印
    }

    // 提取工具调用
    if len(delta.ToolCalls) > 0 {
      toolCall = &delta.ToolCalls[0]
    }
  }

  fmt.Println("\n\n📝 完整回答:", fullText)
  if toolCall == nil {
    return
  }

  // ======================
  // 6. 解析并调用 MCP 工具
  // ======================
  fmt.Println("\n🔧 模型决定调用工具:", toolCall.Function.Name)

  var args map[string]any
  _ = json.Unmarshal([]byte(toolCall.Function.Arguments), &args)

  req := mcp.CallToolRequest{}
  req.Params.Name = toolCall.Function.Name
  req.Params.Arguments = args

  // 执行 MCP
  result, err := client.CallTool(ctx, req)
  if err != nil {
    panic(err)
  }

  fmt.Println("✅ MCP 返回结果:", result.Content)
}

// ======================
// 工具转换:MCP → OpenAI 格式
// ======================
func convertToOpenAITools(mcpTools []mcp.Tool) []openai.Tool {
  var tools []openai.Tool
  for _, t := range mcpTools {
    tool := openai.Tool{
      Type: openai.ToolTypeFunction,
      Function: &openai.FunctionDefinition{
        Name:        t.Name,
        Description: t.Description,
        Parameters:  t.InputSchema,
      },
    }
    tools = append(tools, tool)
  }
  return tools
}

扩展——抓包分析agent与mcp服务端的通信

用wireshark抓到127.0.0.1:8080端口的流量

分析agent到mcp服务的通信过程

sequenceDiagram
    title MCP Agent 与 MCP Server 通信时序图

    participant Agent as Agent (Claude Code)
    participant Server as MCP Server (127.0.0.1:8080)

    %% 初始化阶段
    rect rgb(240, 248, 255)
        Note over Agent,Server: 阶段一:初始化 (Initialization)

        Agent->>+Server: POST /mcp<br/>initialize<br/>{protocolVersion, capabilities, clientInfo}
        Server-->>-Agent: 200 OK<br/>{protocolVersion, capabilities, serverInfo}<br/>Mcp-Session-Id

        Agent->>Server: POST /mcp<br/>notifications/initialized
        Server-->>Agent: 202 Accepted

        Note over Agent,Server: 建立 SSE 长连接用于接收服务端消息
        Agent->>+Server: GET /mcp (text/event-stream)
        Server-->>-Agent: 200 OK (持续连接中...)
    end

    %% 工具发现阶段
    rect rgb(255, 250, 240)
        Note over Agent,Server: 阶段二:工具发现 (Tool Discovery)

        Agent->>+Server: POST /mcp<br/>tools/list
        Server-->>-Agent: 200 OK<br/>{tools: [{name: "add", ...}]}
    end

    %% 工具调用阶段
    rect rgb(240, 255, 240)
        Note over Agent,Server: 阶段三:工具调用 (Tool Invocation)

        Agent->>+Server: POST /mcp<br/>tools/call<br/>{name: "add", arguments: {a: 111233101, b: 34423}}
        Server-->>-Agent: 200 OK<br/>{content: [{type: "text", text: "111267524"}]}
    end

初始化

POST /mcp HTTP/1.1
host: 127.0.0.1:8080
connection: keep-alive
accept: application/json, text/event-stream
content-type: application/json
user-agent: claude-code/2.1.92 (cli)
accept-language: *
sec-fetch-mode: cors
accept-encoding: gzip, deflate
content-length: 304

{"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{"roots":{},"elicitation":{}},"clientInfo":{"name":"claude-code","title":"Claude Code","version":"2.1.92","description":"Anthropic's agentic coding tool","websiteUrl":"https://claude.com/claude-code"}},"jsonrpc":"2.0","id":0}
HTTP/1.1 200 OK
Content-Type: application/json
Mcp-Session-Id: mcp-session-05a1cd0c-5dac-407d-8ab6-4ec289d6df7e
Date: Thu, 09 Apr 2026 01:49:45 GMT
Content-Length: 150

{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{}},"serverInfo":{"name":"............","version":"1.0.0"}}}

POST /mcp HTTP/1.1
host: 127.0.0.1:8080
connection: keep-alive
accept: application/json, text/event-stream
content-type: application/json
mcp-protocol-version: 2025-11-25
mcp-session-id: mcp-session-05a1cd0c-5dac-407d-8ab6-4ec289d6df7e
user-agent: claude-code/2.1.92 (cli)
accept-language: *
sec-fetch-mode: cors
accept-encoding: gzip, deflate
content-length: 54

{"method":"notifications/initialized","jsonrpc":"2.0"}
HTTP/1.1 202 Accepted
Date: Thu, 09 Apr 2026 01:49:45 GMT
Content-Length: 0


GET /mcp HTTP/1.1
host: 127.0.0.1:8080
connection: keep-alive
accept: text/event-stream
mcp-protocol-version: 2025-11-25
mcp-session-id: mcp-session-05a1cd0c-5dac-407d-8ab6-4ec289d6df7e
user-agent: claude-code/2.1.92 (cli)
accept-language: *
sec-fetch-mode: cors
accept-encoding: gzip, deflate


HTTP/1.1 200 OK
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
Date: Thu, 09 Apr 2026 01:49:45 GMT
Transfer-Encoding: chunked

查询工具列表

POST /mcp HTTP/1.1
host: 127.0.0.1:8080
connection: keep-alive
accept: application/json, text/event-stream
content-type: application/json
mcp-protocol-version: 2025-11-25
mcp-session-id: mcp-session-05a1cd0c-5dac-407d-8ab6-4ec289d6df7e
user-agent: claude-code/2.1.92 (cli)
accept-language: *
sec-fetch-mode: cors
accept-encoding: gzip, deflate
content-length: 46

{"method":"tools/list","jsonrpc":"2.0","id":1}
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 09 Apr 2026 01:49:45 GMT
Content-Length: 352

{"jsonrpc":"2.0","id":1,"result":{"tools":[{"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":true},"description":"............","inputSchema":{"properties":{"a":{"description":"......","type":"number"},"b":{"description":"......","type":"number"}},"required":["a","b"],"type":"object"},"name":"add"}]}}

调用时

POST /mcp HTTP/1.1
host: 127.0.0.1:8080
connection: keep-alive
accept: application/json, text/event-stream
content-type: application/json
mcp-protocol-version: 2025-11-25
mcp-session-id: mcp-session-05a1cd0c-5dac-407d-8ab6-4ec289d6df7e
user-agent: claude-code/2.1.92 (cli)
accept-language: *
sec-fetch-mode: cors
accept-encoding: gzip, deflate
content-length: 191

{"method":"tools/call","params":{"name":"add","arguments":{"a":111233101,"b":34423},"_meta":{"claudecode/toolUseId":"call_7a461a0e4a9b4fb0b010c197","progressToken":2}},"jsonrpc":"2.0","id":2}
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 09 Apr 2026 02:00:54 GMT
Content-Length: 83

{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"111267524"}]}}

扩展——agent与大模型的通信

因为很多模型都是https的端点,直接抓包都是加了密的,没法看

不过我发现智谱的目标,可以http访问,我们可以直接抓出口网卡到智谱对应ip的流量,就可以直接看到明文数据了

如果目标只能https访问,那就只能自己写一个http代理,抓到代理的流量,这样也可以看到明文数据

package main

import (
  "flag"
  "fmt"
  "io"
  "log"
  "net"
  "net/http"
  "time"
)

// handleHTTP 处理 HTTP 代理请求
func handleHTTP(w http.ResponseWriter, r *http.Request) {
  client := &http.Client{
    Timeout: 30 * time.Second,
  }

  // 创建新的请求,从原始请求读取 body
  req, err := http.NewRequest(r.Method, r.URL.String(), r.Body)
  if err != nil {
    http.Error(w, "创建请求失败: "+err.Error(), http.StatusInternalServerError)
    return
  }
  req.Header = r.Header.Clone()
  // Host 由 http.NewRequest 从 URL 自动处理

  resp, err := client.Do(req)
  if err != nil {
    http.Error(w, "代理请求失败: "+err.Error(), http.StatusInternalServerError)
    log.Println("HTTP转发错误:", err)
    return
  }
  defer resp.Body.Close()

  // 将目标服务器响应复制回客户端
  for k, v := range resp.Header {
    w.Header()[k] = v
  }
  w.WriteHeader(resp.StatusCode)
  io.Copy(w, resp.Body)
}

// handleHTTPS 处理 HTTPS CONNECT 请求(建立隧道)
func handleHTTPS(w http.ResponseWriter, r *http.Request) {
  log.Printf("CONNECT 请求: Host=%s", r.Host)

  // 连接目标服务器
  destConn, err := net.DialTimeout("tcp", r.Host, 10*time.Second)
  if err != nil {
    log.Printf("连接目标服务器失败: %v", err)
    http.Error(w, "连接目标服务器失败: "+err.Error(), http.StatusServiceUnavailable)
    return
  }

  // 劫持 HTTP 连接,直接进行 TCP 数据转发
  hijacker, ok := w.(http.Hijacker)
  if !ok {
    log.Println("不支持 Hijack")
    destConn.Close()
    http.Error(w, "不支持 Hijack", http.StatusInternalServerError)
    return
  }
  clientConn, _, err := hijacker.Hijack()
  if err != nil {
    log.Printf("劫持连接失败: %v", err)
    destConn.Close()
    http.Error(w, "劫持连接失败: "+err.Error(), http.StatusServiceUnavailable)
    return
  }

  // 发送标准的 CONNECT 200 响应
  _, err = clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
  if err != nil {
    log.Printf("发送 CONNECT 响应失败: %v", err)
    clientConn.Close()
    destConn.Close()
    return
  }
  log.Printf("CONNECT 隧道已建立,开始转发数据")

  // 设置读写超时,防止连接被长时间占用
  _ = clientConn.SetDeadline(time.Now().Add(60 * time.Second))
  _ = destConn.SetDeadline(time.Now().Add(60 * time.Second))

  // 双向转发数据
  done := make(chan struct{})
  go func() {
    io.Copy(destConn, clientConn)
    destConn.Close()
    close(done)
  }()
  io.Copy(clientConn, destConn)
  clientConn.Close()
  <-done
}

// proxyHandler 代理入口
func proxyHandler(w http.ResponseWriter, r *http.Request) {
  log.Printf("请求: Method=%s Host=%s RemoteAddr=%s", r.Method, r.Host, r.RemoteAddr)

  if r.Method == http.MethodConnect {
    handleHTTPS(w, r)
  } else {
    handleHTTP(w, r)
  }
}

func main() {
  port := flag.Int("port", 17890, "代理服务器监听端口")
  flag.Parse()

  listenAddr := fmt.Sprintf("0.0.0.0:%d", *port)
  log.Println("代理服务器启动,监听地址:", listenAddr)

  err := http.ListenAndServe(listenAddr, http.HandlerFunc(proxyHandler))
  if err != nil {
    log.Fatal("代理服务启动失败:", err)
  }
}

然后把它配置到cc switch的代理上

然后就可以用wireshark抓127.0.0.1:17890上的流量了

扩展——好用mcp服务分享

可以在这些网站搜自己需要的mcp服务

mcp市场:https://app-7z9xujpw4t1d.appmiaoda.com/marketplace

mcp盒子:https://mcpbox.net/

mcp搜索:https://mcps.live/

mcpso:https://mcp.so/

阿里云mcp:https://bailian.console.aliyun.com/cn-beijing?tab=mcp#/mcp-market

github上的mcp广场:https://github.com/punkpeye/awesome-mcp-servers

好用的mcp推荐

  1. MCP Server Playwright

https://github.com/VikashLoomba/MCP-Server-Playwright?tab=readme-ov-file#tools

浏览器自动化的mcp,可以直接操作浏览器,绝对夯爆了,为AI助手提供完整的浏览器控制能力。这个服务适合需要AI执行复杂网页交互任务的场景,如自动化测试、数据采集、表单填写等。

  1. GitHub MCP Server

GitHub MCP Server是开发者使用最广泛的MCP服务之一,它允许AI助手直接与GitHub仓库进行交互。通过这个服务,用户可以自动化代码审查、处理拉取请求、管理代码仓库等操作。开发者只需用自然语言描述需求,AI就能自动执行相应的GitHub操作,例如创建分支、提交代码、审核PR等

  1. PostgreSQL MCP Server

PostgreSQL MCP Server为AI助手提供了直接查询和管理PostgreSQL数据库的能力。用户可以通过自然语言描述数据查询需求,AI自动生成SQL语句并执行查询。这个服务对于需要频繁与数据库交互的数据分析师、后端开发者来说非常实用,能够显著减少编写SQL语句的时间,同时提高查询的准确性。

参考文档

mcp中文站 https://mcpcn.com/

mcp广场 https://www.modelscope.cn/mcp

mcp中文文档 https://mcp-docs.cn/docs/getting-started/intro