GoSuda

MCP host'u Anlamak İçin Kısa Bir Bakış

By snowmerak
views ...

MCP Nedir

MCP, Anthropic tarafından Claude için geliştirilmiş bir protokoldür. MCP, Model Context Protocol'ün kısaltması olup, LLM'in harici eylemleri veya kaynakları aktif olarak talep etmesini sağlayan bir protokoldür. MCP, kelimenin tam anlamıyla yalnızca istek ve yanıt veren bir protokol olduğundan, süreç ve yürütme geliştirici tarafından yapılmalıdır.

Dahili İşleyiş Hakkında

Dahili işleyişi açıklamadan önce, Gemini Function Calling konusuna değineceğiz. Gemini Function Calling de MCP ile aynı şekilde LLM'in harici eylemleri proaktif olarak çağırmasına olanak tanır. O zaman, "Neden Function Calling'i buraya getirdik?" diye bir soru akla gelebilir. Getirme sebebimiz, Function Calling'in MCP'den önce çıkmış olması ve aynı OpenAPI şemasını kullanmaları nedeniyle uyumlu olmaları, dolayısıyla karşılıklı işleyişlerinin benzer olacağının tahmin edilmesiydi. Bu nedenle, Gemini Function Calling'in açıklamasının daha ayrıntılı olması faydalı olacağı düşünülerek buraya dahil edildi.

FunctionCalling

Genel akış şu şekildedir:

  1. Bir fonksiyon tanımlanır.
  2. Fonksiyon tanımı, Prompt ile birlikte Gemini'ye gönderilir.
    1. "Kullanıcı prompt'u, fonksiyon bildirim(ler)i ile birlikte modele gönderilir. Model, isteği analiz eder ve bir fonksiyon çağrısının faydalı olup olmayacağını belirler. Eğer öyleyse, yapılandırılmış bir JSON nesnesi ile yanıt verir."
  3. Gemini, gerektiğinde fonksiyon çağrısı talep eder.
    1. Gemini, gerektiğinde fonksiyon çağrısı için adı ve parametreleri çağırana iletir.
    2. Çağıran, yürütüp yürütmeyeceğine karar verebilir.
      1. Çağırıp geçerli bir değer döndürmek mi?
      2. Çağırmadan, çağrılmış gibi veri döndürmek mi?
      3. Yoksa sadece göz ardı etmek mi?
  4. Gemini, yukarıdaki süreçte birden fazla fonksiyonu aynı anda çağırabilir veya bir fonksiyonu çağırdıktan sonra sonuçları görüp tekrar çağrı yapma gibi eylemleri gerçekleştirir ve talep eder.
  5. Sonuç olarak düzenlenmiş bir yanıt geldiğinde sonlanır.

Bu akış, genel olarak MCP ile uyumludur. Bu durum, MCP'nin eğitiminde de benzer şekilde açıklanmaktadır. Ollama araçları da benzerdir.

Ve gerçekten de şans eseri, bu üç araç; ollama tools, MCP ve Gemini Function Calling, şema yapılarını o kadar çok paylaşmaktadır ki, sadece bir MCP uygulayarak üç yerde de kullanılabilmektedir.

Ayrıca, hepsinin paylaştığı bir dezavantaj vardır. Sonuçta, modelin yürütülmesi söz konusu olduğundan, kullandığınız modelin durumu iyi değilse, fonksiyonları çağırmayabilir, garip bir şekilde çağırabilir veya MCP sunucusuna DoS saldırısı gibi hatalı davranışlarda bulunabilir.

Go ile MCP Host

mark3lab's mcphost

Go'da, mark3lab adlı bir kuruluş tarafından geliştirilmekte olan mcphost bulunmaktadır.

Kullanımı oldukça basittir.

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

Kurulumdan sonra, $HOME/.mcp.json dosyasını aşağıdaki gibi oluşturun:

 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}

Ve sonra aşağıdaki gibi bir ollama modeliyle çalıştırın. Elbette, öncesinde gerekirse ollama pull mistral-small ile modeli indirin.

Claude veya qwen2.5'i önermekle birlikte, ben şu an için mistral-small'ı tavsiye ediyorum.

1mcphost -m ollama:mistral-small

Ancak bu şekilde çalıştırıldığında, yalnızca CLI ortamında soru-cevap şeklinde kullanılabilir. Bu nedenle, mcphost kodunu değiştirerek daha programlanabilir bir şekilde çalışmasını sağlayacağız.

mcphost Fork'lama

Daha önce de görüldüğü gibi, mcphost, MCP'yi kullanarak meta veri çıkarma ve fonksiyon çağırma işlevlerini içerir. Bu nedenle, LLM'i çağırma kısmı, MCP sunucusunu yönetme kısmı ve mesaj geçmişini yönetme kısmı gereklidir.

İlgili kısımları içeren paket, aşağıdaki Runner'dır:

 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}

İlgili kısmın dahili tanımına ayrıntılı olarak bakmayacağız. Ancak neredeyse isimleriyle aynıdır.

 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}

Burada kullanılacak mcpClients ve tools için lütfen bu dosyayı kontrol edin.provider olarak ollama'nınkini kullanacağımız için lütfen bu dosyayı kontrol edin.

Ana yemek Run metodudur.

  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}

Kodun kendisi, bu dosyadaki bazı kod parçalarından derlenmiştir.

İçerik kabaca şöyledir:

  1. Prompt ve araç listesi, yürütme veya yanıt oluşturma talebiyle gönderilir.
  2. Bir yanıt oluşturulduğunda, özyineleme durdurulur ve yanıt döndürülür.
  3. LLM bir araç yürütme talebi bırakırsa, host MCP Server'ı çağırır.
  4. Yanıt geçmişe eklenir ve tekrar 1. adıma dönülür.

Sonuç

Şimdiden son mu?

Aslında söylenecek çok fazla şey yok. Bu makale, MCP Server'ın nasıl çalıştığına dair genel bir anlayış sağlamak amacıyla yazılmıştır. Umarım bu yazı, MCP host'un işleyişini anlamanıza küçük de olsa yardımcı olmuştur.