Kapitel 20: Agentische Server & Sampling – Wenn der Server die KI fragt
Bisher haben wir MCP so kennengelernt: Der Client (die KI) fragt den Server nach Tools oder Daten. Ein agentischer Server dreht das um: Er nutzt selbst ein Sprachmodell, um seine Aufgabe zu lösen – er plant, ruft eigene Werkzeuge auf, bewertet Zwischenergebnisse. Für den aufrufenden Agenten wird er damit zu einem Sub-Agenten.
MCP kennt dafür zwei Wege:
- Sampling – der Server leiht sich das Modell des Clients (
sampling/createMessage). - Eigenes Modell – der Server spricht ein LLM direkt an, etwa ein lokales Qwen über Ollama.
Stand der Spezifikation
2026-07-28: Sampling ist deprecated (SEP-2577). Es bleibt mindestens zwölf Monate in der Spezifikation, neue Implementierungen sollen es aber nicht mehr einsetzen und stattdessen LLM-Provider-APIs direkt einbinden. Zudem werden Server-an-Client-Anfragen wiesampling/createMessagenur noch über Multi Round-Trip Requests (MRTR, SEP-2322) zugestellt – der frühere Weg, bei dem der Server selbst eine Anfrage an den Client schickte, ist entfallen. Dieses Kapitel beschreibt Sampling deshalb für bestehende Implementierungen und zeigt in Abschnitt 5 den empfohlenen Weg mit eigenem Modell.
Wie belastbar ist das Beispiel? Der Code-Review-Wächter in Abschnitt 5 ist ein funktionsfähiges Lehrbeispiel, aber kein fertiges Produkt:
- Lauffähig und getestet: Der Go-Code kompiliert gegen
modelcontextprotocol/go-sdkv1.8.0. Die Agenten-Schleife haben wir im Oktober 2026 gegen Ollama mitqwen3-coder:30bund zwei weiteren Qwen-Modellen laufen lassen – das Duplikat im Testfall wurde in allen Läufen erkannt (Details in Abschnitt 5.4). Auch die Task-Variante (Abschnitt 5.5) läuft: mit dem Paketmcptasksaus dem mcp-tester, geprüft mitmcp-tester tasks(alle Prüfungen bestanden) und end-to-end mitmcp-tester call --taskgegen das echte Modell.- Nur angedeutet: Der Ähnlichkeitsindex ist eine Schnittstelle (
CodeIndex); im Test lieferte ein Mini-Index schlicht die Dateien eines Verzeichnisses als Kandidaten. Eine echte Suche über ein Repository – etwa mit Code-Embeddings und Vektordatenbank – müssen Sie selbst ergänzen. Ebenso fehlen Konfiguration (Modell und URL sind Konstanten) und Unit-Tests.- Nicht gezeigt: Die Entscheidung pro Anfrage (wenige Dateien synchron, viele als Task) und eigene Fortschrittstexte – beides braucht einen eigenen Task-Store wie in Kapitel 19, weil
mcptaskspro Tool entscheidet und diestatusMessageselbst setzt. Die Sampling-Beispiele in Abschnitt 2 sind JSON aus der Spezifikation, kein getesteter Code.- Vereinfacht: Der Zeilenzähler kennt nur Go-Kommentare und behandelt Code und Kommentar in derselben Zeile nicht gesondert.
1. Was ist Sampling?
Sampling erlaubt es einem MCP-Server, eine „Probe“ (ein Sample) der Modell-Intelligenz zu nehmen. Der Server schickt eine Liste von Nachrichten und Instruktionen an den Client, der Client liefert eine vom LLM generierte Antwort zurück.
Die ursprünglichen Argumente dafür:
- Keine eigenen API-Keys: Der Server nutzt die KI-Anbindung, die der Client bereits hat.
- Kontrolle beim Client: Der Client kann Sampling-Anfragen blockieren, filtern oder dem Nutzer zur Freigabe vorlegen.
- Modellwahl beim Nutzer: Der Client entscheidet, welches Modell antwortet – auch ein lokales.
2. Die Methode `sampling/createMessage`
Seit 2026-07-28 beantwortet der Server eine Client-Anfrage (tools/call, prompts/get oder resources/read) mit einem InputRequiredResult, das die Sampling-Anfrage enthält. Der Client lässt das Modell antworten und wiederholt die ursprüngliche Anfrage mit der Antwort in inputResponses. Läuft der Tool-Aufruf als Task (Kapitel 19), kommt die Sampling-Anfrage stattdessen über inputRequests in tasks/get und die Antwort über tasks/update.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "input_required",
"inputRequests": {
"summary": {
"method": "sampling/createMessage",
"params": {
"messages": [{ "role": "user", "content": { "type": "text", "text": "Fasse diese Daten zusammen: ..." } }],
"systemPrompt": "Sei präzise und nutze Markdown-Tabellen.",
"modelPreferences": { "intelligencePriority": 0.8, "speedPriority": 0.3 },
"maxTokens": 800
}
}
},
"requestState": "AEAD-geschützter Zustand"
}
}Die wichtigsten Parameter:
messages: Der Verlauf (Rollenuser,assistant).systemPrompt: Anweisungen für diese Generierung. Der Client darf ihn ändern oder ignorieren.modelPreferences: Was dem Server wichtig ist –intelligencePriority,speedPriority,costPriority(je 0–1) undhintsmit Modellnamen. Die Auswahl trifft der Client.maxTokens: Pflichtfeld; der Client muss es einhalten.tools/toolChoice: Der Server kann dem Modell eigene Werkzeuge anbieten. Antwortet das Modell mitstopReason: "toolUse", führt der Server die Werkzeuge aus und schickt eine neue Sampling-Anfrage mit dentool_result-Blöcken – eine Agenten-Schleife über das Modell des Clients. Voraussetzung ist die Capabilitysampling.tools.
Der Server legt seinen Zwischenstand in requestState ab, den der Client unverändert zurückschickt. Weil dieser Wert durch den Client läuft, muss der Server ihn wie eine Eingabe eines Angreifers behandeln und mit HMAC oder AEAD gegen Manipulation schützen.
3. Der „Human-in-the-Loop“
Die Spezifikation legt großen Wert darauf, dass Sampling nicht unbemerkt im Hintergrund abläuft. Ein guter Client sollte:
- Den Nutzer informieren: „Server X möchte das Modell nutzen, um eine Nachricht zu generieren.“
- Dem Nutzer erlauben, System-Prompt und Nachrichten zu prüfen und zu bearbeiten.
- Die Antwort vor der Weitergabe an den Server zur Prüfung vorlegen.
- Die Kostenkontrolle beim Nutzer belassen.
Genau hier liegt die Schwäche für agentische Workflows: Eine Agenten-Schleife mit zehn Runden bedeutet bis zu zehn Freigaben. Das ist sicher, aber für einen Sub-Agenten, der unauffällig im Hintergrund prüfen soll, kaum praktikabel – einer der Gründe, warum der direkte Weg zum Modell heute empfohlen wird.
4. Sampling oder eigenes Modell?
| Kriterium | Sampling (Modell des Clients) | Eigenes Modell im Server |
|---|---|---|
Status in 2026-07-28 |
deprecated | empfohlen |
| API-Keys / Modellbetrieb | keine im Server | Server braucht Zugang (Cloud-Key oder lokales Modell) |
| Wer wählt das Modell? | Client bzw. Nutzer | Server-Betreiber |
| Kosten | trägt das Kontingent des Clients | trägt der Server-Betreiber (lokal: nur Hardware) |
| Freigaben | pro Anfrage durch den Menschen | einmal: Vertrauen in den Server |
| Agenten-Schleife | über MRTR-Runden, jede Runde ein Client-Durchlauf | direkt im Server, ohne Umweg |
| Reproduzierbarkeit | Modell kann je Client wechseln | festes Modell, feste Prompts |
Das frühere Paradebeispiel für Sampling – sensible Daten mit einem lokalen Modell verarbeiten, damit sie das Netz nicht verlassen – lässt sich mit eigenem Modell sogar direkter umsetzen: Der Server spricht das lokale Modell selbst an, und die Daten verlassen die Maschine gar nicht erst.
5. Der Server als Sub-Agent: ein Code-Review-Wächter mit lokalem LLM
5.1 Das Szenario
Claude (oder ein anderer Coding-Agent) schreibt Code. Ein zweiter, spezialisierter Agent soll jede Änderung prüfen:
- a) Gibt es diesen Code schon? Agenten schreiben bereitwillig eine dritte Variante von
parseConfig, statt die vorhandene zu finden. - b) Werden die Hausstandards eingehalten? Zum Beispiel: höchstens 250 Codezeilen je Datei, Kommentare zählen nicht.
Der Prüfer soll ein lokales Modell nutzen (etwa Qwen über Ollama): Der Code verlässt die Maschine nicht, jede Prüfung kostet nur Strom, und das Modell des Haupt-Agenten muss seinen Kontext nicht mit dem halben Repository füllen.
Als MCP-Server gebaut, ist dieser Prüfer ein Sub-Agent, den jeder MCP-fähige Host nutzen kann – Claude Code, Cursor, opencode oder eine eigene Anwendung.
5.2 Die Architektur
Haupt-Agent (Claude) MCP-Server "code-review"
──────────────────── ────────────────────────────
schreibt handler.go
tools/call review_code ──────────► ① Regeln im Code
{files: ["handler.go"]} Codezeilen ≤ 250?
◄─ CreateTaskResult (working) ──── ② Agenten-Schleife mit Qwen
sucht ähnlichen Code
tasks/get ───────────────────────► (search_similar_code
◄─ working, statusMessage ──────── → Index)
beurteilt Kandidaten
tasks/get ───────────────────────► ③ Befunde als JSON
◄─ completed, {findings: […]} ────
behebt die BefundeZwei Entwurfsentscheidungen tragen das Ganze:
1. Rechnen statt Denken. Die 250-Zeilen-Regel prüft der Server mit Code, nicht mit dem LLM. Zählen ist deterministisch, schnell und fehlerfrei; ein Sprachmodell würde sich verzählen. Das ist das Muster aus Kapitel 4: Das Modell bekommt nur, was Urteilsvermögen braucht.
| Prüfung | Wer prüft | Warum |
|---|---|---|
| Codezeilen je Datei | Go-Code | exakt zählbar |
| Benennung, Formatierung | Linter (golangci-lint) |
Regelwerk existiert bereits |
| Kandidaten für Duplikate finden | Index (Embeddings, AST-Hashes) | Suche über das ganze Repository |
| „Ist das wirklich dieselbe Logik?“ | lokales LLM | braucht Verständnis |
| Weiche Regeln („eine Verantwortung je Datei“) | lokales LLM | braucht Verständnis |
2. Suchen und Urteilen trennen. Das Modell durchsucht nicht selbst das Repository. Ein Index liefert Kandidaten – etwa über ein lokales Embedding-Modell und eine Vektordatenbank, oder über einen vorhandenen Code-Graph-Server. Das Modell entscheidet nur, ob ein Kandidat wirklich gleichwertig ist. Das hält den Kontext klein, was gerade bei lokalen Modellen mit begrenztem Kontextfenster zählt.
5.3 Implementierung in Go
Der Server folgt dem Hausstandard: Go mit modelcontextprotocol/go-sdk. Ollama bietet eine OpenAI-kompatible Schnittstelle, über die das Modell Werkzeuge aufrufen kann. Das Beispiel ist bewusst kompakt: Der Zeilenzähler kennt nur Go-Kommentare, und CodeIndex steht für einen beliebigen Ähnlichkeitsindex.
package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
const (
maxCodeLines = 250 // Hausstandard: Codezeilen je Datei, Kommentare zählen nicht
ollamaURL = "http://localhost:11434/v1/chat/completions"
reviewModel = "qwen3-coder:30b"
maxTurns = 6 // Obergrenze für die Agenten-Schleife
)
type ReviewIn struct {
Files []string `json:"files" jsonschema:"Pfade der geänderten Dateien"`
}
type Finding struct {
File string `json:"file"`
Rule string `json:"rule"` // z. B. "max-lines", "duplicate", "naming"
Severity string `json:"severity"` // "error" | "warning"
Message string `json:"message"`
}
type ReviewOut struct {
Findings []Finding `json:"findings"`
}
// CodeIndex findet ähnlichen Code im Repository (Embeddings, AST-Hashes, ...).
type CodeIndex interface {
Similar(ctx context.Context, code string, limit int) ([]string, error)
}
// countCodeLines zählt Codezeilen ohne Leerzeilen und Kommentare – deterministisch, ohne LLM.
func countCodeLines(src string) int {
n, inBlock := 0, false
sc := bufio.NewScanner(strings.NewReader(src))
for sc.Scan() {
l := strings.TrimSpace(sc.Text())
switch {
case inBlock:
inBlock = !strings.Contains(l, "*/")
case strings.HasPrefix(l, "/*"):
inBlock = !strings.Contains(l, "*/")
case l == "", strings.HasPrefix(l, "//"):
default:
n++
}
}
return n
}
// Stufe 1: harte Regeln im Code prüfen ("Rechnen statt Denken").
func checkRules(path, src string) []Finding {
if n := countCodeLines(src); n > maxCodeLines {
return []Finding{{File: path, Rule: "max-lines", Severity: "error",
Message: fmt.Sprintf("%d Codezeilen (erlaubt: %d) – entlang einer Naht aufteilen", n, maxCodeLines)}}
}
return nil
}
type chatMsg struct {
Role string `json:"role"`
Content string `json:"content"`
ToolCalls []toolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}
type toolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
}
// Das einzige Werkzeug des Sub-Agenten: im Repository nach ähnlichem Code suchen.
var agentTools = []map[string]any{{
"type": "function",
"function": map[string]any{
"name": "search_similar_code",
"description": "Sucht im Repository nach Code, der dem übergebenen Ausschnitt ähnelt.",
"parameters": map[string]any{
"type": "object",
"properties": map[string]any{"code": map[string]any{"type": "string"}},
"required": []string{"code"},
},
},
}}
func chat(ctx context.Context, msgs []chatMsg) (chatMsg, error) {
body, _ := json.Marshal(map[string]any{"model": reviewModel, "messages": msgs, "tools": agentTools})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, ollamaURL, bytes.NewReader(body))
if err != nil {
return chatMsg{}, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return chatMsg{}, err
}
defer resp.Body.Close()
var out struct {
Choices []struct{ Message chatMsg } `json:"choices"`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil || len(out.Choices) == 0 {
return chatMsg{}, fmt.Errorf("ungültige Antwort vom Modell: %v", err)
}
return out.Choices[0].Message, nil
}
// Stufe 2: Agenten-Schleife mit dem lokalen Modell – Duplikate und weiche Regeln beurteilen.
func reviewWithLLM(ctx context.Context, idx CodeIndex, path, src string) ([]Finding, error) {
msgs := []chatMsg{
{Role: "system", Content: "Du prüfst Go-Code auf Duplikate und Verstöße gegen die Hausregeln. " +
"Nutze search_similar_code für jede neue Funktion. Erlaubte Regeln: duplicate, responsibility, naming. " +
`Antworte am Ende NUR mit JSON: {"findings":[{"rule":"...","severity":"error|warning","message":"..."}]}`},
{Role: "user", Content: "Datei " + path + ":\n\n" + src},
}
for turn := 0; turn < maxTurns; turn++ {
msg, err := chat(ctx, msgs)
if err != nil {
return nil, err
}
msgs = append(msgs, msg)
if strings.Contains(msg.Content, "<function=") { // Werkzeugaufruf als Text statt als tool_calls
msgs = append(msgs, chatMsg{Role: "user", Content: "Rufe Werkzeuge über die Tool-Schnittstelle auf, nicht als Text."})
continue
}
if len(msg.ToolCalls) == 0 { // fertig: Modell liefert das Urteil
var out ReviewOut
if err := json.Unmarshal([]byte(extractJSON(msg.Content)), &out); err != nil {
return nil, fmt.Errorf("Urteil ist kein JSON: %w", err)
}
for i := range out.Findings {
out.Findings[i].File = path
}
return out.Findings, nil
}
for _, tc := range msg.ToolCalls { // Werkzeuge ausführen, Ergebnisse zurückgeben
var args struct{ Code string }
_ = json.Unmarshal([]byte(tc.Function.Arguments), &args)
hits, err := idx.Similar(ctx, args.Code, 5)
result := strings.Join(hits, "\n---\n")
if err != nil {
result = "Fehler: " + err.Error()
}
msgs = append(msgs, chatMsg{Role: "tool", ToolCallID: tc.ID, Content: result})
}
}
return nil, fmt.Errorf("kein Urteil nach %d Runden", maxTurns)
}
// extractJSON schneidet das JSON-Objekt aus der Antwort – lokale Modelle setzen gern ```json-Fences darum.
func extractJSON(s string) string {
if i, j := strings.Index(s, "{"), strings.LastIndex(s, "}"); i >= 0 && j > i {
return s[i : j+1]
}
return s
}
func register(s *mcp.Server, idx CodeIndex) {
mcp.AddTool(s, &mcp.Tool{
Name: "review_code",
Description: "Prüft geänderte Dateien gegen die Hausstandards (max. 250 Codezeilen je Datei) " +
"und sucht im Repository nach bereits vorhandenem, gleichwertigem Code. " +
"Nach jeder Änderung an Go-Dateien aufrufen und die Befunde beheben.",
}, func(ctx context.Context, _ *mcp.CallToolRequest, in ReviewIn) (*mcp.CallToolResult, ReviewOut, error) {
var out ReviewOut
for _, path := range in.Files {
src, err := os.ReadFile(path)
if err != nil {
return nil, ReviewOut{}, err
}
out.Findings = append(out.Findings, checkRules(path, string(src))...)
llm, err := reviewWithLLM(ctx, idx, path, string(src))
if err != nil {
out.Findings = append(out.Findings, Finding{File: path, Rule: "review", Severity: "warning",
Message: "LLM-Prüfung nicht möglich: " + err.Error()})
continue
}
out.Findings = append(out.Findings, llm...)
}
return nil, out, nil // Befunde sind ein Ergebnis, kein Fehler
})
}Drei Details sind wichtiger, als sie aussehen:
- Die Schleife ist begrenzt (
maxTurns). Ein lokales Modell, das sich im Kreis sucht, darf den Haupt-Agenten nicht endlos warten lassen. - Fällt das Modell aus, liefert der Server trotzdem ein Ergebnis. Die deterministischen Regeln greifen immer; der LLM-Teil wird als Warnung gemeldet statt als Fehler.
- Befunde sind kein Fehler. Das Tool gibt
isError: falsezurück – auch bei zehn Verstößen. Die Prüfung hat funktioniert; ihr Ergebnis sind die Befunde. AlsstructuredContent(überReviewOut) kann der Haupt-Agent sie gezielt abarbeiten.
5.4 Erfahrungen aus dem Testlauf
Wir haben die Schleife gegen mehrere lokale Qwen-Modelle unter Ollama laufen lassen. Testfall: eine neue Funktion parseSettings, die fast Zeile für Zeile einer vorhandenen LoadConfig gleicht; der Index liefert LoadConfig als Kandidaten. Alle Modelle haben das Duplikat erkannt. Auf dem Weg dahin zeigten sich aber genau die Probleme, die man bei lokalen Modellen einplanen muss – deshalb enthält der Code oben zwei Absicherungen:
| Beobachtung | Häufigkeit | Gegenmaßnahme |
|---|---|---|
qwen3-coder:30b schreibt den Werkzeugaufruf in seinem eigenen Format (<function=…>) als Text, statt tool_calls zu liefern |
etwa jeder dritte Lauf | Korrekturrunde: Text erkennen, Hinweis an das Modell, Schleife fortsetzen |
Das Urteil kommt in einem Markdown-Block (```json … ```) statt als reines JSON |
modellabhängig | extractJSON schneidet das Objekt heraus |
| Regelnamen schwanken („Duplicated code“, „NoDuplication“, „DRY“ …) | ohne Vorgabe bei jedem Modell | feste Liste erlaubter Regel-IDs im System-Prompt |
Zusätzliche, wechselnde Warnungen (naming, responsibility) zu einer unauffälligen Funktion |
in etwa der Hälfte der Läufe | LLM-Befunde nur als warning werten; error bleibt den deterministischen Regeln und eindeutigen Duplikaten vorbehalten |
Laufzeit: 4–8 s (qwen3-coder:30b, MoE) bis 30–60 s (dichte 27–35B-Modelle) pro Datei |
– | ab mehreren Dateien als Task ausführen (Abschnitt 5.5) |
Mit beiden Absicherungen lief qwen3-coder:30b in allen Wiederholungen fehlerfrei durch. Die Lehre daraus ist allgemeiner als dieses Beispiel: Ein lokaler Sub-Agent braucht dieselbe Robustheit wie jede andere unzuverlässige Schnittstelle – Format prüfen, begrenzt wiederholen, und das Ergebnis so gestalten, dass ein Ausrutscher des Modells nie den deterministischen Teil verdirbt.
Für den Index selbst eignet sich ein lokales Code-Embedding-Modell (etwa jina-embeddings-v2-base-code) zusammen mit einer Vektordatenbank; alternativ lässt sich ein vorhandener Code-Graph-MCP-Server anbinden.
5.5 Synchron oder als Task?
Die Tasks-Extension lässt den Server pro Anfrage entscheiden (Kapitel 19). Das passt hier genau:
- Eine Datei, kleine Änderung: synchron antworten. Die Prüfung dauert ein paar Sekunden, ein Task wäre Overhead.
- Viele Dateien oder großes Repository: ein
CreateTaskResultzurückgeben und die Arbeit in den Task-Store aus Kapitel 19 verlagern. ÜberstatusMessagesieht der Haupt-Agent, was gerade passiert („Datei 3/12: Suche Duplikate …“), und kann in der Zwischenzeit weiterarbeiten. - Rückfrage nötig? Findet der Prüfer eine Funktion, die zu 95 % einer vorhandenen gleicht, kann er über
input_requiredund eine Elicitation fragen: „Soll ich die vorhandene Funktion vorschlagen oder die neue akzeptieren?“
Weil der Server nur dann ein CreateTaskResult liefert, wenn die Anfrage die Extension deklariert, funktioniert derselbe Server auch mit Hosts, die Tasks noch nicht unterstützen – sie bekommen eben synchron die Antwort.
Umsetzung mit `mcptasks`
Das go-sdk bringt noch keine Task-Implementierung mit, aber die nötigen Erweiterungspunkte (Kapitel 19, „Anschluss an das go-sdk“). Das Paket mcptasks aus dem mcp-tester nutzt sie; der Review-Server wird damit ohne Änderung am Handler task-fähig:
func main() {
caps := &mcp.ServerCapabilities{Tools: &mcp.ToolCapabilities{}}
mcptasks.Declare(caps)
s := mcp.NewServer(&mcp.Implementation{Name: "code-review", Version: "0.1.0"},
&mcp.ServerOptions{Capabilities: caps})
register(s, newIndex()) // review_code aus Abschnitt 5.3, newIndex: Ihr CodeIndex
if err := mcptasks.Enable(s, mcptasks.NewStore(), "review_code"); err != nil {
log.Fatal(err)
}
if err := s.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}Der Testlauf mit dem mcp-tester:
$ mcp-tester call review_code --task -c ./review-server --args '{"files":["handler/settings.go"]}'
[TASK] a67c90d1… working: the operation is in progress
[TASK] a67c90d1… completed: the operation completed
StructuredContent:
{ "findings": [
{ "file": "handler/settings.go", "rule": "duplicate", "severity": "error",
"message": "Die Funktion parseSettings … ist eine doppelte Implementation von LoadConfig in config/load.go. …" },
{ "file": "handler/settings.go", "rule": "responsibility", "severity": "warning", "message": "…" } ] }mcp-tester tasks --tool review_code … prüfte zusätzlich die Fehlercodes, das Task-Handle, die Entropie der ID, die dauerhafte Anlage, jedes tasks/get und die Statusübergänge – alle Prüfungen bestanden.
Zwei Grenzen von mcptasks sollten Sie kennen: Das Paket macht ein Tool immer zum Task, wenn der Client die Extension deklariert – die Entscheidung „wenige Dateien synchron“ ist damit nicht möglich. Und die statusMessage setzt das Paket selbst; „Datei 3/12 …“ braucht einen eigenen Store nach dem Muster aus Kapitel 19.
5.6 Wie der Haupt-Agent den Prüfer benutzt
Ein Sub-Agent nützt nur, wenn er auch aufgerufen wird. Dafür gibt es drei Stufen, von weich zu hart:
- Tool-Beschreibung: „Nach jeder Änderung an Go-Dateien aufrufen“ steht direkt in der
description. Das Modell liest sie bei jeder Tool-Auswahl. - Projektanweisung: Ein Satz in
AGENTS.mdoderCLAUDE.md(„Vor jedem Commitreview_codeüber alle geänderten Dateien ausführen und alleerror-Befunde beheben“) macht den Aufruf zur Arbeitsregel. - Erzwingen über den Host: Manche Hosts bieten Hooks, die nach jedem Schreibvorgang automatisch ein Kommando ausführen (etwa die
PostToolUse-Hooks von Claude Code). Damit hängt die Prüfung nicht mehr davon ab, dass das Modell daran denkt. Das ist allerdings Host-Funktionalität, kein MCP.
6. MCP-Sub-Agent oder Sub-Agent des Hosts?
Viele Hosts bringen eigene Sub-Agenten mit (Claude Code, Agent SDKs, IDE-Agenten). Wann lohnt sich der Weg über MCP?
| Kriterium | Sub-Agent des Hosts | Sub-Agent als MCP-Server |
|---|---|---|
| Modell | meist das Modell des Hosts | frei wählbar, auch lokal (Qwen, Llama, …) |
| Daten | gehen an den Modellanbieter des Hosts | bleiben auf Wunsch auf der eigenen Maschine |
| Kosten je Aufruf | Tokens des Hauptmodells | lokal: nahezu null |
| Wiederverwendung | an einen Host gebunden | jeder MCP-fähige Host |
| Werkzeuge | die des Hosts | eigene, eng zugeschnittene (Index, Linter) |
| Deterministische Teile | schwer abzusichern | im Server-Code garantiert |
| Einrichtung | eine Konfigurationsdatei | Server betreiben, Modell bereitstellen |
| Qualität | Spitzenmodell | lokales Modell – für eng umrissene Prüfungen meist ausreichend |
Als Faustregel: Offene, kreative Teilaufgaben („recherchiere“, „entwirf“) gehören zum Host-Sub-Agenten mit starkem Modell. Eng umrissene, wiederkehrende Prüfungen mit festen Regeln – Standards, Duplikate, Lizenzen, Sicherheitsmuster – sind ideale MCP-Sub-Agenten: Sie profitieren von deterministischem Code, eigenen Werkzeugen und einem günstigen lokalen Modell, und sie laufen in jedem Host gleich.
Fazit
Agentische Server machen MCP-Server von passiven Werkzeugkästen zu aktiven Mitarbeitern. Mit 2026-07-28 hat sich der empfohlene Weg verschoben: weg vom Sampling über das Modell des Clients, hin zum Server, der sein Modell selbst mitbringt – gern ein lokales. Zusammen mit der Tasks-Extension aus Kapitel 19 entsteht daraus ein vollwertiger Sub-Agent: Der Haupt-Agent delegiert, der Server prüft mit einer Mischung aus Code und Modell, und das Ergebnis kommt strukturiert zurück.
← Kapitel 19: Tasks | Inhaltsverzeichnis | Nächstes Kapitel: Elicitation →
Copyright Michael Lechner – 2026-10-09 (Sampling deprecated & MRTR nach Spezifikation 2026-07-28, neuer Abschnitt „Der Server als Sub-Agent“)