MCP host'unu biraz anlamak

MCP Nedir
MCP, Anthropic tarafından Claude için geliştirilmiş bir protokoldür. MCP, Model Context Protocol ifadesinin kısaltması olup LLM'nin dış dünyadan aktif olarak bir eylem veya kaynak talep edebilmesini sağlayan bir protokoldür. MCP, tam anlamıyla yalnızca istek ve yanıt veren bir protokol olduğu için bu sürecin ve yürütmenin geliştirici tarafından gerçekleştirilmesi gerekir.
İç İşleyiş Hakkında
İç işleyişi açıklamadan önce Gemini Function Calling üzerinde durmak istiyorum. Gemini Function Calling de MCP ile aynı şekilde LLM'nin dış eylemleri öncülük ederek çağırmasına olanak tanır. Peki neden Function Calling'i ele aldığımız sorusu akla gelebilir. Bunu ele almamın nedeni, Function Calling'in MCP'den önce ortaya çıkmış olması ve her ikisinin de OpenAPI şemasını kullanması nedeniyle uyumlu olmaları, dolayısıyla birbirlerine benzer bir işleyişe sahip olabilecekleri düşüncesidir. Ayrıca Gemini Function Calling'in açıklamalarının nispeten daha ayrıntılı olması nedeniyle yardımcı olacağı düşünülerek ele alınmıştır.

Genel akış şu şekildedir:
- Fonksiyonu tanımlarız.
- Fonksiyon tanımını bir istemle birlikte Gemini'ye göndeririz.
- "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."
- Gemini gerekirse fonksiyon çağrısı talep eder.
- Gemini gerekli görürse, fonksiyon çağrısı için gereken ad ve parametreleri çağırana iletilir.
- Çağıran taraf çalıştırıp çalıştırmamaya karar verebilir.
- Çağırıp geçerli bir değer döndürmek
- Çağırmadan, çağrılmış gibi veri döndürmek
- Tamamen yok saymak
- Gemini yukarıdaki süreçte bir seferde birden fazla fonksiyonu çağırabilir veya fonksiyon çağrısının sonucuna bakarak yeniden çağrı yapabilir; bu tür işlemleri gerçekleştirir ve talep eder.
- Sonuçta düzenli bir yanıt elde edildiğinde süreç sona erer.
Bu akış genel olarak MCP ile örtüşmektedir. Bu durum, MCP eğitiminde de benzer biçimde açıklanmaktadır. Ollama tools da benzer bir yapıya sahiptir.
Ve gerçekten şanslıyız ki bu üç araç, yani ollama tools, MCP ve Gemini Function Calling, şema yapısını neredeyse ortaklaşa kullandığından, yalnızca MCP'yi uygulayarak üçünde de kullanabilirsiniz.
Ayrıca hepsinin ortak bir dezavantajı vardır. Sonuçta çalıştırmayı model yaptığı için kullandığınız modelin durumu kötüyse, fonksiyonu hiç çağırmayabilir, yanlış çağırabilir veya MCP sunucusuna DoS saldırısı gibi yanlış davranışlarda bulunabilir.
Go ile Yazılmış MCP Host
mark3lab'in mcphost'u
Go dünyasında, mark3lab adlı 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ı oluşturup aşağıdaki gibi yazarsınız.
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}
Ardından aşağıdaki gibi ollama modeliyle çalıştırırsınız.
Tabii ki bundan önce gerekirse ollama pull mistral-small komutuyla modeli indirirsiniz.
Genel olarak claude veya qwen2.5 önerilir, ancak ben şimdilik mistral-small'ı öneriyorum.
1mcphost -m ollama:mistral-small
Ancak bu şekilde çalıştırdığınızda yalnızca CLI ortamında soru-cevap biçiminde kullanabilirsiniz.
Bu nedenle mcphost kodunu değiştirerek daha programlanabilir biçimde çalışabilmesini sağlayacağız.
mcphost Forku
Zaten gördüğünüz üzere mcphost, MCP'yi kullanarak meta verileri çıkarma ve fonksiyon çağırma özelliklerini içermektedir. Bu nedenle LLM'yi çağıran kısım, MCP sunucusunu yöneten kısım ve mesaj geçmişini yöneten kısım gereklidir.
İlgili kısımları alarak aşağıdaki paketteki Runner yapısını oluşturduk.
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}
Bu kısmın iç tanımlamalarını ayrıca incelemeyeceğiz. Ancak neredeyse isimleri kendisini açıklamaktadı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 hakkında ilgili dosyaya bakmanızı rica ederim.provider olarak ollama'nın sağlayıcısını kullanacağız, bu nedenle ilgili dosyaya bakmanızı rica ederim.
Ana yemek ise 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, ilgili dosyadaki bazı bölümlerin bir araya getirilmesiyle oluşturulmuştur.
İçeriği kabaca şöyledir:
- Araç listesini istemle birlikte göndererek çalıştırma olup olmadığını veya yanıt üretilmesini talep ederiz.
- Yanıt üretildiğinde özyinelemeyi durdurur ve döndürürüz.
- LLM bir araç çalıştırma talebi bırakırsa, host MCP Server'ı çağırır.
- Yanıtı geçmişe ekleyip tekrar 1. adıma döneriz.
Sonuç
Şimdiden mi bitti?
Aslında söylenecek pek çok şey yok. Bu yazı, esas olarak MCP Server'ın nasıl çalıştığına dair bir anlayış kazandırmak amacıyla hazırlanmıştır. Bu yazının, MCP host'un işleyişini anlamanızda size az da olsa yardımcı olmasını umuyorum.