---
id: a1fad13de24c90924cfa9e25e9f986cb
author: snowmerak
title: 稍微了解一下 MCP host
description: 本文旨在帮助读者理解 Anthropic 的 MCP 协议以及用 Go 实现的 MCP 主机（mcphost）。
language: zh
date: 2025-04-08T00:59:42.416582136Z
path: /blog/posts/a-little-understanding-of-mcp-host-zb952aae0
category: ai
---

## 什么是 MCP

[MCP](https://modelcontextprotocol.io/introduction)是由 Anthropic 为 Claude 开发的协议。MCP 是 Model Context Protocol 的简称,是一种使 LLM 能够主动向外部请求操作或资源的协议。MCP 本质上只是传递请求与响应的协议,因此其过程与执行需要由开发者自行实现。

### 关于内部工作机制

在说明内部工作机制之前,先简要介绍一下 [Gemini Function Calling](https://ai.google.dev/gemini-api/docs/function-calling)。Gemini Function Calling 与 MCP 相同,也允许 LLM 主动调用外部操作。那么您可能会疑惑,为何要引入 Function Calling 呢?引入它的原因在于,Function Calling 比 MCP 更早推出,并且二者都使用 OpenAPI 模式,具有兼容性,因此我推测两者的运行方式应当相似。由于 Gemini Function Calling 的说明相对更为详尽,因此认为其会有所帮助,故将其引入。

![FunctionCalling](/assets/images/mcp_with_csharp/function-calling-overview.png)

整体流程如下。

1. 定义函数。
2. 将函数定义与提示词一同发送给 Gemini。
   1. "Send user prompt along with the function declaration(s) to the model. It analyzes the request and determines if a function call would be helpful. If so, it responds with a structured JSON object."
3. 如果 Gemini 认为有必要,则请求调用函数。
   1. 如果 Gemini 认为有必要,调用方将接收用于函数调用的名称与参数。
   2. 调用方可以决定是否执行。
      1. 执行调用并返回有效的值
      2. 不执行调用,而是返回仿佛已调用一样的数据
      3. 直接忽略
4. Gemini 在上述过程中可以一次调用多个函数,或在调用函数后根据结果再次调用等,执行并请求相应操作。
5. 当得到整理完毕的回答时,流程结束。

该流程与 MCP 基本一致。[MCP 教程](https://modelcontextprotocol.io/tutorials/building-mcp-with-llms)中也有类似的说明。ollama tools 同样与此类似。

而且非常幸运的是,ollama tools、MCP、Gemini Function Calling 这三种工具的模式结构几乎是共通的,因此只需实现 MCP,便可在这三处全部使用。

另外,所有方案都共有一个缺点。归根结底,函数是由模型执行的,因此如果您使用的模型状态不佳,可能会出现不调用函数、错误调用函数,或对 MCP 服务器发起 DoS 攻击等异常行为。

## 基于 Go 的 MCP 主机

### mark3lab 的 mcphost

在 Go 中,有由 mark3lab 组织开发的 [mcphost](https://github.com/mark3labs/mcphost)。

使用方法非常简单。

```sh
go install github.com/mark3labs/mcphost@latest
```

安装完成后,创建 `$HOME/.mcp.json` 文件,并按如下方式编写。

```json
{
  "mcpServers": {
    "sqlite": {
      "command": "uvx",
      "args": [
        "mcp-server-sqlite",
        "--db-path",
        "/tmp/foo.db"
      ]
    },
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/tmp"
      ]
    }
  }
}
```

然后按如下方式使用 ollama 模型运行。  
当然,如有需要,请先通过 `ollama pull mistral-small` 下载模型。

> 一般推荐 Claude 或 qwen2.5,但就目前而言,我推荐 mistral-small。

```sh
mcphost -m ollama:mistral-small
```

不过,以这种方式运行时,只能在 CLI 环境中以问答形式使用。  
因此,我们将修改这个 `mcphost` 的代码,使其能够以更具可编程性的方式运行。

### mcphost 分支

如前所述,`mcphost` 包含利用 MCP 提取元数据并调用函数的功能。因此,需要调用 LLM 的部分、操作 MCP 服务器的部分以及管理消息历史的部分。

将对应的部分提取出来后,即为以下包中的 `Runner`。

```go
package runner

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"strings"
	"time"

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

	"github.com/mark3labs/mcphost/pkg/history"
	"github.com/mark3labs/mcphost/pkg/llm"
)

type Runner struct {
	provider   llm.Provider
	mcpClients map[string]*mcpclient.StdioMCPClient
	tools      []llm.Tool

	messages []history.HistoryMessage
}
```

这部分的内部声明就不单独介绍了,基本上从名称即可理解。

```go
func NewRunner(systemPrompt string, provider llm.Provider, mcpClients map[string]*mcpclient.StdioMCPClient, tools []llm.Tool) *Runner {
	return &Runner{
		provider:   provider,
		mcpClients: mcpClients,
		tools:      tools,
		messages: []history.HistoryMessage{
			{
				Role: "system",
				Content: []history.ContentBlock{{
					Type: "text",
					Text: systemPrompt,
				}},
			},
		},
	}
}
```

关于这里使用的 `mcpClients` 与 `tools`,请参阅[对应文件](https://github.com/snowmerak/mcphost/blob/main/internal/runner/from.go)。  
`provider` 将使用 ollama 的实现,请参阅[对应文件](https://github.com/snowmerak/mcphost/blob/main/pkg/llm/ollama/provider.go)。

核心部分是 `Run` 方法。

```go
func (r *Runner) Run(ctx context.Context, prompt string) (string, error) {
	if len(prompt) != 0 {
		r.messages = append(r.messages, history.HistoryMessage{
			Role: "user",
			Content: []history.ContentBlock{{
				Type: "text",
				Text: prompt,
			}},
		})
	}

	llmMessages := make([]llm.Message, len(r.messages))
	for i := range r.messages {
		llmMessages[i] = &r.messages[i]
	}

	const initialBackoff = 1 * time.Second
	const maxRetries int = 5
	const maxBackoff = 30 * time.Second

	var message llm.Message
	var err error
	backoff := initialBackoff
	retries := 0
	for {
		message, err = r.provider.CreateMessage(
			context.Background(),
			prompt,
			llmMessages,
			r.tools,
		)
		if err != nil {
			if strings.Contains(err.Error(), "overloaded_error") {
				if retries >= maxRetries {
					return "", fmt.Errorf(
						"claude is currently overloaded. please wait a few minutes and try again",
					)
				}

				time.Sleep(backoff)
				backoff *= 2
				if backoff > maxBackoff {
					backoff = maxBackoff
				}
				retries++
				continue
			}

			return "", err
		}

		break
	}

	var messageContent []history.ContentBlock

	var toolResults []history.ContentBlock
	messageContent = []history.ContentBlock{}

	if message.GetContent() != "" {
		messageContent = append(messageContent, history.ContentBlock{
			Type: "text",
			Text: message.GetContent(),
		})
	}

	for _, toolCall := range message.GetToolCalls() {
		input, _ := json.Marshal(toolCall.GetArguments())
		messageContent = append(messageContent, history.ContentBlock{
			Type:  "tool_use",
			ID:    toolCall.GetID(),
			Name:  toolCall.GetName(),
			Input: input,
		})

		parts := strings.Split(toolCall.GetName(), "__")

		serverName, toolName := parts[0], parts[1]
		mcpClient, ok := r.mcpClients[serverName]
		if !ok {
			continue
		}

		var toolArgs map[string]interface{}
		if err := json.Unmarshal(input, &toolArgs); err != nil {
			continue
		}

		var toolResultPtr *mcp.CallToolResult
		req := mcp.CallToolRequest{}
		req.Params.Name = toolName
		req.Params.Arguments = toolArgs
		toolResultPtr, err = mcpClient.CallTool(
			context.Background(),
			req,
		)

		if err != nil {
			errMsg := fmt.Sprintf(
				"Error calling tool %s: %v",
				toolName,
				err,
			)
			log.Printf("Error calling tool %s: %v", toolName, err)

			toolResults = append(toolResults, history.ContentBlock{
				Type:      "tool_result",
				ToolUseID: toolCall.GetID(),
				Content: []history.ContentBlock{{
					Type: "text",
					Text: errMsg,
				}},
			})

			continue
		}

		toolResult := *toolResultPtr

		if toolResult.Content != nil {
			resultBlock := history.ContentBlock{
				Type:      "tool_result",
				ToolUseID: toolCall.GetID(),
				Content:   toolResult.Content,
			}

			var resultText string
			for _, item := range toolResult.Content {
				if contentMap, ok := item.(map[string]interface{}); ok {
					if text, ok := contentMap["text"]; ok {
						resultText += fmt.Sprintf("%v ", text)
					}
				}
			}

			resultBlock.Text = strings.TrimSpace(resultText)

			toolResults = append(toolResults, resultBlock)
		}
	}

	r.messages = append(r.messages, history.HistoryMessage{
		Role:    message.GetRole(),
		Content: messageContent,
	})

	if len(toolResults) > 0 {
		r.messages = append(r.messages, history.HistoryMessage{
			Role:    "user",
			Content: toolResults,
		})

		return r.Run(ctx, "")
	}

	return message.GetContent(), nil
}
```

代码本身是从[对应文件](https://github.com/snowmerak/mcphost/blob/main/cmd/mcp.go)中的部分代码拼接而成的。

大致内容如下。

1. 将提示词与工具列表一同发送,请求判断是否执行或生成回答。
2. 生成回答后,停止递归并返回。
3. 如果 LLM 留下了工具执行请求,主机将调用 MCP Server。
4. 将结果添加到历史记录中,然后返回第 1 步。

## 结语

> already 结束了?

其实没有太多要说的。本文旨在帮助您大致理解 MCP Server 的工作方式。希望本文能对您理解 MCP host 的运行机制有所帮助,哪怕只是一点点。
