MCP host'u Anlamak İçin Kısa Bir Bakış
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.
Genel akış şu şekildedir:
- Bir fonksiyon tanımlanır.
- Fonksiyon tanımı, Prompt ile birlikte Gemini'ye gönderilir.
- "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."
- Gemini, gerektiğinde fonksiyon çağrısı talep eder.
- Gemini, gerektiğinde fonksiyon çağrısı için adı ve parametreleri çağırana iletir.
- Çağıran, yürütüp yürütmeyeceğine karar verebilir.
- Çağırıp geçerli bir değer döndürmek mi?
- Çağırmadan, çağrılmış gibi veri döndürmek mi?
- Yoksa sadece göz ardı etmek mi?
- 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.
- 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:
- Prompt ve araç listesi, yürütme veya yanıt oluşturma talebiyle gönderilir.
- Bir yanıt oluşturulduğunda, özyineleme durdurulur ve yanıt döndürülür.
- LLM bir araç yürütme talebi bırakırsa, host MCP Server'ı çağırır.
- 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.