---
id: a1fad13de24c90924cfa9e25e9f986cb
author: snowmerak
title: Getting a Little Familiar with MCP Host
description: This article is intended to help readers understand Anthropic's MCP protocol and the MCP host (mcphost) implemented in Go.
language: en
date: 2025-04-08T00:59:42.416582136Z
path: /blog/posts/a-little-understanding-of-mcp-host-zb952aae0
category: ai
---

## What is MCP

[MCP](https://modelcontextprotocol.io/introduction) is a protocol developed by Anthropic for Claude. MCP is short for Model Context Protocol, and it is a protocol that allows an LLM to actively request actions or resources from the outside. Since MCP is literally just a protocol for giving requests and responses, the process and execution must be handled by the developer.

### On the internal operation

Before explaining the internal operation, I would like to touch on [Gemini Function Calling](https://ai.google.dev/gemini-api/docs/function-calling). Gemini Function Calling, like MCP, allows an LLM to proactively invoke external operations. You may then wonder why Function Calling was brought up at all. The reason is that Function Calling appeared before MCP, and because it uses the OpenAPI schema in the same way, the two are compatible, so I assumed their internal operations would be similar. Consequently, since the explanation of Gemini Function Calling is relatively more detailed, I brought it up as it seems likely to be helpful.

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

The overall flow is as follows.

1. Define the functions.
2. Send the function definitions to Gemini along with the prompt.
   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. If Gemini deems it necessary, it requests a function call.
   1. When Gemini deems it necessary, the caller receives the name and parameters for the function call.
   2. The caller can decide whether to execute or not.
      1. Whether to call it and return a valid value
      2. Whether to not call it and instead return data as if it had been called
      3. Whether to simply ignore it
4. Gemini performs and requests operations such as calling multiple functions at once, or calling again after viewing the results of a function call, during the above process.
5. The process terminates when a well-organized answer is produced.

This flow is generally consistent with MCP. This is similarly explained in [MCP's tutorial](https://modelcontextprotocol.io/tutorials/building-mcp-with-llms). It is also similar to ollama tools.

And, thankfully, these three tools, ollama tools, MCP, and Gemini Function Calling, share a nearly common schema structure, so by implementing only MCP, you can use it in all three places.

Ah, and there is a shortcoming that all of them share. Because the model is what actually executes the calls, if the model you use is in poor condition, it may fail to call functions, call them incorrectly, or even launch a DoS attack against the MCP server, among other malfunctions.

## MCP Host in Go

### mark3lab's mcphost

In Go, there is [mcphost](https://github.com/mark3labs/mcphost), which is being developed by an organization called mark3lab.

Usage is very simple.

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

After installation, create a `$HOME/.mcp.json` file and write the following.

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

Then run it with an ollama model as follows.  
Of course, before that, if necessary, download the model with `ollama pull mistral-small`.

> Claude or qwen2.5 are generally recommended, but at present I recommend mistral-small.

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

However, if you run it this way, you can only use it in a question-and-answer format in the CLI environment.  
Therefore, we will modify the code of this `mcphost` so that it can operate in a more programmable way.

### Forking mcphost

As you have already confirmed, `mcphost` includes functionality that uses MCP to extract metadata and call functions. Therefore, it requires a part that calls the LLM, a part that handles the MCP server, and a part that manages the message history.

The part corresponding to these was extracted into the `Runner` of the following package.

```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
}
```

I will not look at the internal declarations of this part separately. However, they are nearly identical to their names.

```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,
				}},
			},
		},
	}
}
```

For the `mcpClients` and `tools` used here, please check [that file](https://github.com/snowmerak/mcphost/blob/main/internal/runner/from.go).  
For `provider`, we will use the one from ollama, so please check [that file](https://github.com/snowmerak/mcphost/blob/main/pkg/llm/ollama/provider.go).

The main course is the `Run` method.

```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
}
```

The code itself is a patchwork of some code from [that file](https://github.com/snowmerak/mcphost/blob/main/cmd/mcp.go).

The contents are roughly as follows.

1. Send the tool list along with the prompt to request either execution or response generation.
2. When a response is generated, stop the recursion and return it.
3. If the LLM leaves a tool execution request, the host calls the MCP Server.
4. Add the response to the history and return to step 1.

## Conclusion

> Already over?

In fact, I don't have that much to say. This article was written mainly to help you understand roughly how an MCP Server operates. I hope this article helped you, even a little, to understand how the MCP host operates.
