---
id: a1fad13de24c90924cfa9e25e9f986cb
author: snowmerak
title: MCPホストを少し理解してみる
description: Anthropicの MCPプロトコルと、Goで実装されたMCPホスト(mcphost)の理解を助けることを目的とした記事です。
language: ja
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の3つのツールはスキーマ構造をほぼ共有しているため、MCPを一つ実装するだけで3か所すべてに利用できるということです。

また、全てに共通する欠点があります。結局のところモデルが実行を担うため、利用しているモデルの状態が良くない場合、関数を呼び出さなかったり、誤った形で呼び出したり、MCPサーバーにDOS攻撃のような負荷をかけたりするなどの誤動作を起こす可能性があります。

## Goで作られたMCPホスト

### mark3lab's 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サーバーを呼び出します。
4. 応答を履歴に追加し、再び1番に戻ります。

## おわりに

> もう終わり？

実のところ、言うべきことはそれほど多くありません。MCPサーバーがどのように動作するかについて大まかに理解していただくために書いた文章です。この文章が、皆さんのMCPホストの動作を理解する一助になっていれば幸いです。
