---
id: a1fad13de24c90924cfa9e25e9f986cb
author: snowmerak
title: MCP-isännän hieman ymmärtäminen
description: Tämä teksti auttaa ymmärtämään Anthropicin MCP-protokollaa ja Go-kielellä toteutettua MCP-isäntää (mcphost).
language: fi
date: 2025-04-08T00:59:42.416582136Z
path: /blog/posts/a-little-understanding-of-mcp-host-zb952aae0
category: ai
---

## Mitä MCP on

[MCP](https://modelcontextprotocol.io/introduction) on Anthropicin claudea varten kehittämä protokolla. MCP on lyhenne sanoista Model Context Protocol, ja se on protokolla, jonka avulla LLM voi aktiivisesti pyytää ulkoisia toimintoja tai resursseja. MCP on sanasta sanaan vain protokolla, joka välittää pyyntöjä ja vastauksia, joten prosessin ja toteutuksen tekee kehittäjän itse.

### Sisäinen toiminta

Ennen sisäisen toiminnan selittämistä käyn lyhyesti läpi [Gemini Function Callingin](https://ai.google.dev/gemini-api/docs/function-calling). Gemini Function Calling mahdollistaa samoin kuin MCP sen, että LLM voi itse aktiivisesti kutsua ulkoisia toimintoja. Saatat kysyä, miksi toin Function Callingin esiin. Syy on se, että Function Calling ilmestyi aiemmin kuin MCP, ja koska se hyödyntää samaa OpenAPI-skeemaa, ne ovat yhteensopivia, joten arvioin niiden toiminnan olevan samankaltaista. Gemini Function Callingin selitys on myös suhteellisen yksityiskohtainen, joten arvioin sen olevan hyödyllinen, ja siksi toin sen esiin.

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

Kokonaisuudessaan kulku on seuraava.

1. Määritellään funktio.
2. Lähetetään funktiomääritelmä kehotteen kanssa Geminille.
   1. Lähetä käyttäjän kehote yhdessä funktiomääritelmien kanssa mallille. Se analysoi pyynnön ja päättää, olisiko funktiokutsusta hyötyä. Jos on, se vastaa jäsennellyllä JSON-objektilla.
3. Gemini pyytää funktiokutsua tarvittaessa.
   1. Jos Gemini tarvitsee funktiokutsua, kutsuja saa sen nimen ja parametrit.
   2. Kutsuja päättää, miten toimii.
      1. Suorittaako kutsun ja palauttaako pätevän arvon
      2. Palauttaako datan ilman kutsua ikään kuin kutsu olisi tehty
      3. Vai jättääkö kutsun kokonaan huomiotta
4. Gemini pyytää yllä olevassa prosessissa kerralla useita funktioita tai suorittaa lisäkutsuja tulosten perusteella.
5. Lopulta, kun saadaan jäsennelty vastaus, prosessi päättyy.

Tämä kulku vastaa yleisesti MCP:tä. Sitä selittää samankaltaisesti myös [MCP:n opetusohjelma](https://modelcontextprotocol.io/tutorials/building-mcp-with-llms). Sama pätee myös ollama toolsiin.

Onneksi nämä kolme työkalua, ollama tools, MCP ja Gemini Function Calling, jakavat lähes saman skeemarakenteen, joten pelkällä MCP:n toteutuksella voi käyttää sitä kaikissa kolmessa.

Kaikilla on myös yhteinen heikkous. Koska malli itse suorittaa kutsut, jos käyttämäsi malli on heikko, se voi jättää funktiot kutsumatta, kutsua niitä oudosti tai aiheuttaa muita virheitä, kuten lähettää DOS-hyökkäyksen MCP-palvelimelle.

## Go-kielinen MCP-isäntä

### mark3lab's mcphost

Go-kielellä on [mcphost](https://github.com/mark3labs/mcphost), jota kehitetään mark3lab-organisaatiossa.

Käyttö on hyvin yksinkertaista.

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

Asennuksen jälkeen luo tiedosto `$HOME/.mcp.json` ja kirjoita siihen seuraavaa:

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

Sen jälkeen suoritat sen ollama-mallilla seuraavasti:  
Tietenkin tarvittaessa lataat ennen sitä mallin komennolla `ollama pull mistral-small`.

> Yleisesti ottaen suosittelen claudea tai qwen2.5:tä, mutta itse suosittelen tällä hetkellä mistral-smallia.

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

Tällöin sitä voi kuitenkin käyttää vain kysymys-vastaus-tyyppisesti CLI-ympäristössä.  
Siksi muokkaamme `mcphost`-koodia, jotta siitä saadaan ohjelmallisempi.

### mcphost-haarukointi

Kuten jo totesimme, `mcphost` sisältää toiminnon, joka hyödyntää MCP:tä metatietojen poimimiseen ja funktioiden kutsumiseen. Siksi tarvitaan osa, joka kutsuu LLM:ää, osa, joka käsittelee MCP-palvelinta, ja osa, joka hallinnoi viestihistoriaa.

Tämä osa on otettu paketista, jonka `Runner` on.

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

Sen sisäistä määrittelyä ei käsitellä tässä erikseen. Se on kuitenkin lähes nimensä mukainen.

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

Käytettävien `mcpClients`- ja `tools`-osien osalta katso [tämä tiedosto](https://github.com/snowmerak/mcphost/blob/main/internal/runner/from.go).  
`provider`-osassa käytetään ollaman versiota, joten katso [tämä tiedosto](https://github.com/snowmerak/mcphost/blob/main/pkg/llm/ollama/provider.go).

Pääosa on `Run`-metodi.

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

Koodi on osittain koottu [tämän tiedoston](https://github.com/snowmerak/mcphost/blob/main/cmd/mcp.go) koodista.

Sisältö on karkeasti seuraava:

1. Lähetetään työkalulista kehotteen kanssa ja pyydetään päättämään suorituksesta tai luomaan vastaus.
2. Kun vastaus on luotu, rekursio pysäytetään ja vastaus palautetaan.
3. Jos LLM jättää pyynnön työkalun suorittamisesta, isäntä kutsuu MCP-palvelinta.
4. Vastaus lisätään historiaan ja palataan kohtaan 1.

## Lopuksi

> Jo loppu?

Itse asiassa minulla ei ole kovin paljon sanottavaa. Tämä teksti on kirjoitettu lähinnä auttamaan ymmärtämään, miten MCP-palvelin karkeasti toimii. Toivottavasti tämä edes vähän auttoi ymmärtämään MCP-isännän toimintaa.
