---
id: a1fad13de24c90924cfa9e25e9f986cb
author: snowmerak
title: Forstå MCP-hosten lidt bedre
description: Dette er en artikel, der har til formål at hjælpe med at forstå Anthropics MCP-protokol og MCP-værten (mcphost), som er implementeret i Go.
language: da
date: 2025-04-08T00:59:42.416582136Z
path: /blog/posts/a-little-understanding-of-mcp-host-zb952aae0
category: ai
---

## Hvad er MCP

[MCP](https://modelcontextprotocol.io/introduction) er en protokol udviklet af Anthropic til Claude. MCP er en forkortelse af Model Context Protocol, og er en protokol, der gør det muligt for en LLM aktivt at anmode om handlinger eller ressourcer fra det eksterne miljø. Da MCP i bogstavelig forstand blot er en protokol til afsendelse af anmodninger og svar, skal udvikleren selv stå for processen og udførelsen.

### Om den interne funktionsmåde

Inden vi forklarer den interne funktionsmåde, vil vi kort gennemgå [Gemini Function Calling](https://ai.google.dev/gemini-api/docs/function-calling). Gemini Function Calling gør det ligesom MCP muligt for LLM'en selv at tage initiativ til at kalde eksterne handlinger. Man kan så spørge, hvorfor Function Calling overhovedet er medtaget. Grunden er, at Function Calling kom før MCP, og at det, fordi begge benytter OpenAPI-skemaet, er kompatibelt; vi antog derfor, at deres interne funktionsmåde ville ligne hinanden. Da forklaringen af Gemini Function Calling er relativt detaljeret, vurderede vi, at den ville være nyttig, og derfor er den medtaget.

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

Den overordnede proces er som følger:

1. Definer en funktion.
2. Send funktionsdefinitionen til Gemini sammen med en 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. Gemini anmoder om et funktionskald, hvis det er nødvendigt.
   1. Hvis Gemini har brug for det, modtager kalderen navnet og parametrene til funktionskaldet.
   2. Kalderen kan selv beslutte, om kaldet skal udføres.
      1. Udfør kaldet og returner en gyldig værdi
      2. Udfør ikke kaldet, men returner data som om det var udført
      3. Ignorer det helt
4. Gemini kan i ovenstående proces anmode om flere funktioner på én gang, eller kalde en funktion, se resultatet og derefter kalde igen.
5. Processen afsluttes, når der foreligger et velformuleret svar.

Denne proces stemmer generelt overens med MCP. Det beskrives også på lignende vis i [MCP-vejledningen](https://modelcontextprotocol.io/tutorials/building-mcp-with-llms). Det samme gælder for ollama tools.

Heldigvis har disse tre værktøjer, ollama tools, MCP og Gemini Function Calling, næsten identisk skemastruktur, så det er muligt at bruge alle tre steder ved kun at implementere MCP.

En fælles ulempe skal også nævnes. Da det i sidste ende er modellen, der udfører kaldene, kan en model i dårlig form undlade at kalde funktionen, kalde den forkert eller udføre fejlagtige handlinger, såsom at sende DoS-anmodninger til MCP-serveren.

## MCP-værter i Go

### mark3labs mcphost

I Go findes [mcphost](https://github.com/mark3labs/mcphost), som udvikles af organisationen mark3lab.

Brugen er meget enkel.

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

Efter installationen opretter du filen `$HOME/.mcp.json` og skriver følgende i den:

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

Derefter kører du det med en ollama-model således:  
Hvis det er nødvendigt, kan du dog først hente modellen med `ollama pull mistral-small`.

> Generelt anbefales Claude eller qwen2.5, men i øjeblikket anbefaler jeg mistral-small.

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

Men når du kører det på denne måde, kan du kun anvende det som et spørgsmål-og-svar-forløb i CLI-miljøet.  
Derfor vil vi nu ændre koden i `mcphost`, så den kan fungere mere programmerbart.

### Fork af mcphost

Som allerede set indeholder `mcphost` funktionalitet til at udtrække metadata og kalde funktioner via MCP. Der er derfor brug for en del, der kalder LLM'en, en del der håndterer MCP-serveren, og en del der administrerer beskedhistorikken.

Den del er hentet fra `Runner` i følgende pakke.

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

Den interne deklaration af denne del gennemgår vi ikke nærmere. Den er dog næsten identisk med navnene.

```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 `mcpClients` og `tools`, der anvendes her, se [den relevante fil](https://github.com/snowmerak/mcphost/blob/main/internal/runner/from.go).  
Til `provider` bruger vi ollamas, så se [den relevante fil](https://github.com/snowmerak/mcphost/blob/main/pkg/llm/ollama/provider.go).

Hovedretten er metoden `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
}
```

Koden er sammensat af dele fra [den relevante fil](https://github.com/snowmerak/mcphost/blob/main/cmd/mcp.go).

Indholdet kan overordnet opsummeres således:

1. Send prompten sammen med værktøjslisten og anmod om enten eksekvering eller generering af et svar.
2. Når et svar er genereret, stoppes rekursionen, og svaret returneres.
3. Hvis LLM'en anmoder om værktøjskørsel, kalder værten MCP-serveren.
4. Tilføj svaret til historikken og gå tilbage til punkt 1.

## Afslutning

> Allerede slut?

Der er faktisk ikke så meget mere at sige. Artiklen er skrevet for at give et overblik over, hvordan en MCP-server fungerer. Jeg håber, at den har hjulpet dig, bare lidt, med at forstå, hvordan en MCP-host fungerer.
