GoSuda

Getting a Little Familiar with MCP Host

By snowmerak views ...

What is MCP

MCP 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. 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

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. 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, which is being developed by an organization called mark3lab.

Usage is very simple.

1go install github.com/mark3labs/mcphost@latest

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

 1{
 2  "mcpServers": {
 3    "sqlite": {
 4      "command": "uvx",
 5      "args": [
 6        "mcp-server-sqlite",
 7        "--db-path",
 8        "/tmp/foo.db"
 9      ]
10    },
11    "filesystem": {
12      "command": "npx",
13      "args": [
14        "-y",
15        "@modelcontextprotocol/server-filesystem",
16        "/tmp"
17      ]
18    }
19  }
20}

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.

1mcphost -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.

 1package runner
 2
 3import (
 4	"context"
 5	"encoding/json"
 6	"fmt"
 7	"log"
 8	"strings"
 9	"time"
10
11	mcpclient "github.com/mark3labs/mcp-go/client"
12	"github.com/mark3labs/mcp-go/mcp"
13
14	"github.com/mark3labs/mcphost/pkg/history"
15	"github.com/mark3labs/mcphost/pkg/llm"
16)
17
18type Runner struct {
19	provider   llm.Provider
20	mcpClients map[string]*mcpclient.StdioMCPClient
21	tools      []llm.Tool
22
23	messages []history.HistoryMessage
24}

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

 1func NewRunner(systemPrompt string, provider llm.Provider, mcpClients map[string]*mcpclient.StdioMCPClient, tools []llm.Tool) *Runner {
 2	return &Runner{
 3		provider:   provider,
 4		mcpClients: mcpClients,
 5		tools:      tools,
 6		messages: []history.HistoryMessage{
 7			{
 8				Role: "system",
 9				Content: []history.ContentBlock{{
10					Type: "text",
11					Text: systemPrompt,
12				}},
13			},
14		},
15	}
16}

For the mcpClients and tools used here, please check that file.
For provider, we will use the one from ollama, so please check that file.

The main course is the Run method.

  1func (r *Runner) Run(ctx context.Context, prompt string) (string, error) {
  2	if len(prompt) != 0 {
  3		r.messages = append(r.messages, history.HistoryMessage{
  4			Role: "user",
  5			Content: []history.ContentBlock{{
  6				Type: "text",
  7				Text: prompt,
  8			}},
  9		})
 10	}
 11
 12	llmMessages := make([]llm.Message, len(r.messages))
 13	for i := range r.messages {
 14		llmMessages[i] = &r.messages[i]
 15	}
 16
 17	const initialBackoff = 1 * time.Second
 18	const maxRetries int = 5
 19	const maxBackoff = 30 * time.Second
 20
 21	var message llm.Message
 22	var err error
 23	backoff := initialBackoff
 24	retries := 0
 25	for {
 26		message, err = r.provider.CreateMessage(
 27			context.Background(),
 28			prompt,
 29			llmMessages,
 30			r.tools,
 31		)
 32		if err != nil {
 33			if strings.Contains(err.Error(), "overloaded_error") {
 34				if retries >= maxRetries {
 35					return "", fmt.Errorf(
 36						"claude is currently overloaded. please wait a few minutes and try again",
 37					)
 38				}
 39
 40				time.Sleep(backoff)
 41				backoff *= 2
 42				if backoff > maxBackoff {
 43					backoff = maxBackoff
 44				}
 45				retries++
 46				continue
 47			}
 48
 49			return "", err
 50		}
 51
 52		break
 53	}
 54
 55	var messageContent []history.ContentBlock
 56
 57	var toolResults []history.ContentBlock
 58	messageContent = []history.ContentBlock{}
 59
 60	if message.GetContent() != "" {
 61		messageContent = append(messageContent, history.ContentBlock{
 62			Type: "text",
 63			Text: message.GetContent(),
 64		})
 65	}
 66
 67	for _, toolCall := range message.GetToolCalls() {
 68		input, _ := json.Marshal(toolCall.GetArguments())
 69		messageContent = append(messageContent, history.ContentBlock{
 70			Type:  "tool_use",
 71			ID:    toolCall.GetID(),
 72			Name:  toolCall.GetName(),
 73			Input: input,
 74		})
 75
 76		parts := strings.Split(toolCall.GetName(), "__")
 77
 78		serverName, toolName := parts[0], parts[1]
 79		mcpClient, ok := r.mcpClients[serverName]
 80		if !ok {
 81			continue
 82		}
 83
 84		var toolArgs map[string]interface{}
 85		if err := json.Unmarshal(input, &toolArgs); err != nil {
 86			continue
 87		}
 88
 89		var toolResultPtr *mcp.CallToolResult
 90		req := mcp.CallToolRequest{}
 91		req.Params.Name = toolName
 92		req.Params.Arguments = toolArgs
 93		toolResultPtr, err = mcpClient.CallTool(
 94			context.Background(),
 95			req,
 96		)
 97
 98		if err != nil {
 99			errMsg := fmt.Sprintf(
100				"Error calling tool %s: %v",
101				toolName,
102				err,
103			)
104			log.Printf("Error calling tool %s: %v", toolName, err)
105
106			toolResults = append(toolResults, history.ContentBlock{
107				Type:      "tool_result",
108				ToolUseID: toolCall.GetID(),
109				Content: []history.ContentBlock{{
110					Type: "text",
111					Text: errMsg,
112				}},
113			})
114
115			continue
116		}
117
118		toolResult := *toolResultPtr
119
120		if toolResult.Content != nil {
121			resultBlock := history.ContentBlock{
122				Type:      "tool_result",
123				ToolUseID: toolCall.GetID(),
124				Content:   toolResult.Content,
125			}
126
127			var resultText string
128			for _, item := range toolResult.Content {
129				if contentMap, ok := item.(map[string]interface{}); ok {
130					if text, ok := contentMap["text"]; ok {
131						resultText += fmt.Sprintf("%v ", text)
132					}
133				}
134			}
135
136			resultBlock.Text = strings.TrimSpace(resultText)
137
138			toolResults = append(toolResults, resultBlock)
139		}
140	}
141
142	r.messages = append(r.messages, history.HistoryMessage{
143		Role:    message.GetRole(),
144		Content: messageContent,
145	})
146
147	if len(toolResults) > 0 {
148		r.messages = append(r.messages, history.HistoryMessage{
149			Role:    "user",
150			Content: toolResults,
151		})
152
153		return r.Run(ctx, "")
154	}
155
156	return message.GetContent(), nil
157}

The code itself is a patchwork of some code from that file.

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.