Das MCP-Handbuch

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 wie sampling/createMessage nur 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-sdk v1.8.0. Die Agenten-Schleife haben wir im Oktober 2026 gegen Ollama mit qwen3-coder:30b und 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 Paket mcptasks aus dem mcp-tester, geprüft mit mcp-tester tasks (alle Prüfungen bestanden) und end-to-end mit mcp-tester call --task gegen 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 mcptasks pro Tool entscheidet und die statusMessage selbst 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 (Rollen user, 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) und hints mit 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 mit stopReason: "toolUse", führt der Server die Werkzeuge aus und schickt eine neue Sampling-Anfrage mit den tool_result-Blöcken – eine Agenten-Schleife über das Modell des Clients. Voraussetzung ist die Capability sampling.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:

  1. Den Nutzer informieren: „Server X möchte das Modell nutzen, um eine Nachricht zu generieren.“
  2. Dem Nutzer erlauben, System-Prompt und Nachrichten zu prüfen und zu bearbeiten.
  3. Die Antwort vor der Weitergabe an den Server zur Prüfung vorlegen.
  4. 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 Befunde

Zwei 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: false zurück – auch bei zehn Verstößen. Die Prüfung hat funktioniert; ihr Ergebnis sind die Befunde. Als structuredContent (über ReviewOut) 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 CreateTaskResult zurückgeben und die Arbeit in den Task-Store aus Kapitel 19 verlagern. Über statusMessage sieht 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_required und 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:

  1. Tool-Beschreibung: „Nach jeder Änderung an Go-Dateien aufrufen“ steht direkt in der description. Das Modell liest sie bei jeder Tool-Auswahl.
  2. Projektanweisung: Ein Satz in AGENTS.md oder CLAUDE.md („Vor jedem Commit review_code über alle geänderten Dateien ausführen und alle error-Befunde beheben“) macht den Aufruf zur Arbeitsregel.
  3. 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“)

Lizenz: CC BY-NC 4.0