Getting a Little Familiar with MCP Host

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.

The overall flow is as follows.
- Define the functions.
- Send the function definitions to Gemini along with the prompt.
- "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."
- If Gemini deems it necessary, it requests a function call.
- When Gemini deems it necessary, the caller receives the name and parameters for the function call.
- The caller can decide whether to execute or not.
- Whether to call it and return a valid value
- Whether to not call it and instead return data as if it had been called
- Whether to simply ignore it
- 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.
- 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.
- Send the tool list along with the prompt to request either execution or response generation.
- When a response is generated, stop the recursion and return it.
- If the LLM leaves a tool execution request, the host calls the MCP Server.
- 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.