Eigener MCP-Server für Claude Code in Go bauen
Ein eigener MCP-Server verwandelt unscharfe, token-teure LLM-Arbeit — Aggregationen, Berechnungen und konsistente Datenbank-Einträge — in einen einzigen deterministischen Tool-Aufruf. Statt das Modell Kommandos absetzen, Text lesen und raten zu lassen, geben Sie ihm ein typisiertes Tool mit beschriebenem Schema und einem validierten Ergebnis. Und Sie bauen es an einem Nachmittag als einzelne Go-Binary.
Das hier ist eine Anleitung. Genau dieses Muster liefern wir in binzaar aus, unserem quelloffenen Gerüst für Micro-Apps: eine Go-Binary, die zugleich ein MCP-Server über stdio, eine Terminal-Oberfläche und eine eingebettete Datenbank über einer einzigen Use-Case-Schicht ist. Im Folgenden bauen Sie Schritt für Schritt Ihren eigenen, mit den echten mark3labs/mcp-go- und go.etcd.io/bbolt-APIs, die wir verwenden.
Warum ein MCP-Server einem CLI-Tool oder blossem Prompten überlegen ist
Ein MCP-Tool gibt dem Modell ein typisiertes Schema, validierte Eingaben und ein strukturiertes Ergebnis. Ein CLI-Tool gibt ihm ein Ratespiel. Wenn Claude Code ein CLI bedient, muss es sich die Flags merken, die Binary ausführen, den freien Text auf stdout zerlegen und hoffen, dass das Zerlegen geklappt hat. Jeder dieser Schritte kostet Token und kann still scheitern.
Mit MCP ruft das Modell eine beschriebene Funktion auf und bekommt JSON zurück. Weniger Token, weniger Roundtrips und eine Ausgabe, die das Modell nicht verpfuschen kann.
flowchart TB
subgraph CLI["CLI-Tool — das Modell setzt Kommandos ab"]
direction TB
M1["Claude Code"] --> G1["Flags raten"] --> R1["Binary ausführen"] --> P1["stdout-Text zerlegen"] --> X1["hoffen, dass es passt"]:::bad
end
subgraph MCP["MCP-Tool — ein typisierter Aufruf"]
direction TB
M2["Claude Code"] --> C2["add_lead(name, email) aufrufen"] --> V2["Schema validiert Eingabe"] --> S2["strukturiertes JSON-Ergebnis"]:::good
end
classDef good fill:#494fdf,stroke:#376cd5,color:#ffffff,stroke-width:2px
classDef bad fill:#1e293b,stroke:#64748b,color:#f1f5f9,stroke-width:1.5px
Es gibt einen zweiten Grund: .mcp.json ist ein gemeinsamer Standard. Claude Code liest sie, und andere MCP-Clients tun es auch. Ein Server, den Sie schreiben, bedient jedes Tool, das das Protokoll spricht — Sie bauen keine Claude-eigene Integration.
Was ein MCP-Server wirklich löst
Der Kern-Gewinn ist Effizienz und Konsistenz. Manche Arbeit ist für ein LLM per Prompt teuer und für eine kleine Binary billig und korrekt zu erledigen. Verlagern Sie diese Arbeit in ein Tool.
Drei Fälle zahlen sich sofort aus:
- Aggregationen und Berechnungen. Hunderte Zeilen im Prompt zu summieren, zu gruppieren oder zu sortieren verbrennt Token und lädt zu Rechenfehlern ein. Ein
report_totals-Tool liefert die Zahl. - Datenbank-Einträge mit Modell-Konsistenz. Das ist der grosse Punkt. Lässt man das Modell Zeilen von Hand schreiben, driftet es — hier ein umbenanntes Feld, dort ein umgebogener Typ. Geben Sie ihm ein einziges
create_lead- oderadd_install-Tool, das Schema und Validierung erzwingt, und jeder Schreibvorgang ist identisch. - Begrenzte, durchsuchbare Oberflächen. Statt eine ganze Tabelle in den Kontext zu kippen, nimmt ein
search-Tool eine Suchanfrage und liefert eine Seite zurück. Das Modell sieht nur, wonach es gefragt hat.
Determinismus ist der Punkt. Derselbe Aufruf mit derselben Eingabe liefert dasselbe Ergebnis, und das Modell kann keine Form erfinden, die Sie nicht definiert haben.
Das Go-Projekt aufsetzen
Beginnen Sie mit einer Vorlage, damit die Schichtung vom ersten Tag an stimmt. binzaar init legt ein eingebettetes Claude-Code-Kit in Ihr Verzeichnis: ein .claude/ mit den entscheidenden Regeln (mcp-server.md, db-rules.md) und einem spezifikationsgetriebenen Workflow — /product-idea schreibt die Spezifikation, /app-init gerüstet das Go-Modul, /app-spec-sync hält Code und Spezifikation im Gleichschritt.
Die Form ist eine Domäne hinter drei Gesichtern, alle durch dieselbe eine Datei gestützt:
flowchart TB
DB["eingebettete bbolt-DB<br/>(eine Datei, kein Server)"]:::core
UC["Use-Case-Schicht<br/>(Ihre Verben)"]:::core
DB --> UC
UC --> MCP["MCP-Server über stdio<br/>(Claude Code, andere Clients)"]:::face
UC --> TUI["Terminal-Oberfläche<br/>(Menschen)"]:::face
classDef core fill:#494fdf,stroke:#376cd5,color:#ffffff,stroke-width:2px
classDef face fill:#1e293b,stroke:#64748b,color:#f1f5f9,stroke-width:1.5px
Zwei Bibliotheken tragen die Last:
- MCP:
github.com/mark3labs/mcp-go— Protokolltypen und das Server-Objekt. - Speicherung:
go.etcd.io/bbolt— ein eingebetteter, ACID-konformer Key/Value-Speicher in einer Datei. Reines Go, kein Server, kein cgo. Aliasen Sie ihn alsboltund greifen Sie nie zum veraltetengithub.com/boltdb/bolt.
Das Ergebnis ist eine Binary mit einem serve- (oder mcp-)Modus, der den stdio-Server ausführt, plus einem Standardmodus für die TUI. main bleibt schlank; cmd/ parst den Modus und öffnet die eine Datenbankdatei.
Ihren Bedarf als Tools beschreiben
Benennen Sie Ihre Use-Cases als Verben und machen Sie aus jedem Verb ein Tool. Stellen Sie zuerst eine Frage: Was muss das Modell tun, das per Prompt teuer ist? Jede Antwort ist ein Tool mit beschriebenem Schema.
Setzen Sie immer eine Beschreibung am Tool und an jedem Parameter — das Modell verlässt sich auf diese Texte, um zu wissen, wann und wie es aufruft. Hier ist ein echtes add_lead-Tool mit der Fehler-Konvention, der wir folgen:
package server
import ( "context"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server")
// New builds a transport-agnostic server; cmd/ picks the transport.func New(name, version string, h *handler) *server.MCPServer { s := server.NewMCPServer(name, version, server.WithToolCapabilities(true), server.WithRecovery(), // a panicking handler can't crash the process )
s.AddTool(mcp.NewTool("add_lead", mcp.WithDescription("Create one lead in the database with validated fields."), mcp.WithString("name", mcp.Required(), mcp.Description("Full name of the lead")), mcp.WithString("email", mcp.Required(), mcp.Description("Contact email")), mcp.WithString("source", mcp.Enum("web", "referral", "event"), mcp.Description("Where the lead came from")), ), h.addLead)
return s}
func (h *handler) addLead(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { name, err := req.RequireString("name") if err != nil { return mcp.NewToolResultError(err.Error()), nil // user error → result, nil } email, err := req.RequireString("email") if err != nil { return mcp.NewToolResultError(err.Error()), nil }
lead, err := h.app.CreateLead(ctx, name, email, req.GetString("source", "web")) if err != nil { return nil, err // infrastructure error → nil, err } return mcp.NewToolResultJSON(lead)}Diese Fehler-Trennung ist wichtig. Eine schlechte Eingabe oder eine verletzte Geschäftsregel liefert NewToolResultError(...), nil — ein Ergebnis, das das Modell lesen und korrigieren kann. Ein Fehler, den das Modell nicht beheben kann (die DB ist unten, die Platte ist voll), liefert nil, err als echten Protokollfehler. Verwechselt man beides, verbirgt man entweder echte Fehler oder führt das Modell bei einem behebbaren Fehler in eine Sackgasse.
Die Tool-Oberfläche feinschleifen
Sobald ein Tool mehr als ein oder zwei Argumente nimmt, hören Sie auf, rohe Parameter zu lesen, und wechseln Sie zu einem typisierten Handler. Definieren Sie eine Eingabe-Struct mit jsonschema-Tags, registrieren Sie sie mit mcp.WithInputSchema[T]() und umschliessen Sie den Handler mit mcp.NewStructuredToolHandler(fn) — die Eingabe wird validiert und gebunden, bevor Ihr Code läuft.
type SearchRequest struct { Query string `json:"query" jsonschema:"required,description=Search text"` Limit int `json:"limit" jsonschema:"description=Max results, default 20"`}Drei Regeln halten einen Server unter einem Agenten ehrlich:
- List-Tools paginieren — nie alles ausschütten. Geben Sie jedem List-Tool eine Suchanfrage, eine Sortierung und eine begrenzte Seitengrösse. Klemmen Sie ein zu grosses
limitauf das Maximum, statt es abzulehnen; das Modell soll Ihre Obergrenze nicht durch einen erneuten Versuch lernen müssen. - Auf stderr protokollieren, nie auf stdout. Bei stdio ist stdout der Protokollkanal. Ein verirrtes
fmt.Printlnbeschädigt den Stream. Schreiben Sie jede Log-Zeile auf stderr. - Die Konstruktion transportunabhängig halten.
internal/serverbaut und registriert Tools ohne jede Erwähnung eines Transports.cmd/wählt stdio:
// cmd/ selects the transport; internal/server stays transport-agnostic.if err := server.ServeStdio(s); err != nil { fmt.Fprintln(os.Stderr, "mcp server:", err) // stderr, not stdout os.Exit(1)}In .mcp.json eintragen
Claude Code entdeckt Ihren Server über eine projekt-lokale .mcp.json. Sie enthält eine mcpServers-Map; jeder Eintrag ist ein stdio-Start — ein command und dessen args — der auf Ihre gebaute Binary in ihrem serve-Modus zeigt:
{ "mcpServers": { "leaddesk": { "command": "/home/you/.local/share/binzaar/bin/leaddesk", "args": ["serve"] } }}Legen Sie diese Datei in das Projekt-Stammverzeichnis, starten Sie Claude Code neu, und Ihre Tools erscheinen. Weil die Datei ein gemeinsamer Standard ist, liest jeder andere MCP-Client in diesem Projekt denselben Eintrag. Lassen Sie command auf einen absoluten Pfad zur Binary zeigen; das serve-Argument ist es, das sie vom TUI-Modus in den stdio-Server umschaltet.
Optional: binzaar erledigt es für Sie
Wenn Sie das lieber nicht von Hand tun, kann binzaar es. Es gerüstet eine brandneue Micro-App aus einer Vorlage und übergibt an den /product-idea-Spezifikations-Workflow, und sein configure_mcp-Schritt schreibt für eine installierte App genau den .mcp.json-Eintrag von oben — er fügt diesen einen Server-Schlüssel hinzu oder ersetzt ihn und lässt jeden anderen Schlüssel in der Datei unangetastet. Der Code und die Regeln sind quelloffen unter github.com/Techthos/binzaar.
Die Kernaussage bleibt in beiden Fällen: Ein eigener MCP-Server macht aus teurem, drift-anfälligem Prompten einen einzigen deterministischen Tool-Aufruf, und eine einzelne Go-Binary mit eingebetteter Datenbank reicht, um ihn auszuliefern. Beginnen Sie mit dem einen Verb, das Sie heute die meisten Token kostet, und geben Sie dem Modell ein Tool dafür.
Wenn Sie beim Entwurf der Tool-Oberfläche für Ihren eigenen Stack Unterstützung möchten, nehmen Sie Kontakt auf.