---
title: "Ahrefs MCP Server: Einrichtung für Claude, Codex und den Rest"
description: "Es gibt zwei Ahrefs-MCP-Server und zwei Schlüsselarten, und die falsche Paarung scheitert lautlos. Die Verdrahtung pro Client, plus die Fallen, die echtes Geld kosten."
author: "Matthias Meyer"
published: 2026-08-03
updated: 2026-08-03
language: de
tags: ["ahrefs", "mcp", "claude-code", "codex", "seo", "api"]
canonical: "https://studiomeyer.io/de/blog/ahrefs-mcp-server-setup"
markdown_versions: ["https://studiomeyer.io/de/blog/ahrefs-mcp-server-setup.md", "https://studiomeyer.io/en/blog/ahrefs-mcp-server-setup.md", "https://studiomeyer.io/es/blog/ahrefs-mcp-server-setup.md"]
publisher: "StudioMeyer, https://studiomeyer.io (llms.txt: https://studiomeyer.io/llms.txt)"
---

# Ahrefs MCP Server: Einrichtung für Claude, Codex und den Rest

Das Erste, was passiert, wenn man Ahrefs an einen KI-Client hängt, ist: nichts. Keine Fehlermeldung, keine Tools, nur ein Server, der verbunden aussieht und stumm bleibt. Bei mir war die Ursache banal: Ahrefs betreibt zwei verschiedene MCP-Server und zwei verschiedene Arten von API-Schlüssel, und nur eine der vier möglichen Kombinationen ist heute die richtige. Welche man erwischt hat, sagt einem niemand.

Das ist die Kurzfassung, warum es diesen Text gibt. Die lange Fassung: Ich habe über einen Abrechnungszeitraum rund 1.100 protokollierte Abrufe durch dieses Ding geschickt, und was am meisten Zeit gekostet hat, war nicht die SEO-Analyse. Es war die Installation.

## Womit man sich eigentlich verbindet

Ahrefs betreibt einen gehosteten MCP-Server unter `https://api.ahrefs.com/mcp/mcp`. Er spricht Streamable HTTP, den aktuellen Transport der Model Context Protocol Spezifikation, und den unterstützt jeder ernsthafte Client. SSE gilt als überholt, darauf sollte man nichts Neues mehr bauen.

Hinter diesem Endpunkt liegt fast alles, was man sonst in der Ahrefs-Weboberfläche anklickt: Site Explorer für Backlinks und organische Keywords, Keywords Explorer für Volumen und Schwierigkeit, Rank Tracker, Site Audit, und die Search Console Anbindung, sofern ein Konto verknüpft ist. In meiner Installation waren das 130 aufrufbare Werkzeuge, also mehr als die Marketing-Seiten nennen, weil der Server gewachsen ist.

Zwei Dinge sollte man vorher wissen. Der Zugang beginnt beim Lite-Tarif, ein kostenloses Testkonto kommt nicht hinein. Und jeder abrechenbare Abruf zieht aus demselben monatlichen Kontingent an API-Units wie die normale API-Nutzung, was bedeutet: Der Chat-Assistent und die Cronjobs essen vom selben Teller. Die Abrechnung teilt sich dabei in drei: Etliche Endpunkte kosten gar nichts, manche einen Pauschalpreis je Anfrage, der Rest pro Zeile. Der letzte Abschnitt handelt davon, sie auseinanderzuhalten.

## Zwei Server, zwei Schlüsselarten, ein stiller Fehlschlag

Das ist der Teil, der meinen ersten Abend gekostet hat.

Es gibt einen älteren lokalen Server, veröffentlicht als `@ahrefs/mcp` auf npm und als `ahrefs/ahrefs-mcp-server` auf GitHub. Dieses Repository ist inzwischen archiviert, und in seiner Readme steht ein Satz, den man zweimal lesen sollte: Es funktioniert ausschließlich mit API-v3-Schlüsseln, und es funktioniert nicht mit MCP-Schlüsseln.

Der gehostete Server verhält sich genau umgekehrt. Er will einen Schlüssel mit MCP-Scope, den man im Ahrefs-Konto getrennt erzeugt. Ahrefs schreibt unmissverständlich, dass API-Schlüssel und MCP-Schlüssel nicht austauschbar sind.

Die Matrix sieht damit so aus. Zwei Felder funktionieren, aber nur eines davon ist heute eine sinnvolle Wahl:

| | API-v3-Schlüssel | MCP-Schlüssel |
|---|---|---|
| **Lokal `@ahrefs/mcp`** | funktionierte, Repo jetzt archiviert | scheitert |
| **Remote `/mcp/mcp`** | scheitert | **richtig** |

Die archivierte Kombination ist weniger kaputt als aufgegeben. Sie läuft weiter, wenn man sie schon hat, wird aber nicht mehr gepflegt, und Ahrefs verweist stattdessen auf den gehosteten Server.

Die Fehlerbilder sind leise. Ein MCP-Schlüssel gegen die REST-API liefert `Unauthorized`, das ist immerhin eine Auskunft. Ein Client, der den Handshake nicht abschließt, zeigt den Server oft einfach mit null Tools an, und dann sucht man einen Tippfehler in einer Konfiguration, die keinen hat.

Wer heute anfängt: gehosteter Server, MCP-Schlüssel, und jede Anleitung ignorieren, die etwas per npm installieren will.

## Anmeldung: OAuth ist der offizielle Weg, Bearer der nützliche

Ahrefs dokumentiert OAuth als Standardweg. Der Client öffnet ein Browser-Fenster, man meldet sich an, die Anmeldedaten werden zwischengespeichert. Für interaktive Arbeit ist das in Ordnung und tatsächlich die bequemste Variante.

Unangenehm wird es in dem Moment, in dem ein geplanter Job sonntagmorgens um sieben Daten ziehen soll. OAuth braucht für die erste Autorisierung einen Menschen vor einem Browser, und danach pflegt man eine Token-Erneuerung, die unbeaufsichtigt weiterlaufen muss. Für alles Kopflose melde ich mich deshalb mit einem Bearer-Token an und schicke den MCP-Schlüssel direkt im `Authorization`-Header. Gleicher Endpunkt, gleiche Tools, kein Browser, nichts zu erneuern.

Die Regel, bei der ich gelandet bin: Bearer für alles, was ohne mich laufen muss, OAuth für den Rechner, vor dem ich sitze. Wer das nur im Chat nutzt, nimmt die OAuth-Anmeldung und überspringt die nächsten Abschnitte.

## Claude Code

Ein Befehl, und die Scope-Angabe entscheidet, ob der Server in diesem Projekt oder in der Benutzer-Konfiguration landet.

```bash
claude mcp add --transport http ahrefs https://api.ahrefs.com/mcp/mcp \
  --header "Authorization: Bearer $AHREFS_API_KEY" -s project
```

Projekt-Scope schreibt in eine `.mcp.json` neben dem Code, was richtig ist, wenn der Schlüssel zu einem Kunden oder einer Site gehört. Diese Datei gehört in die `.gitignore`, bevor ein Schlüssel hineinkommt, denn der Header steht dort im Klartext.

Zwei Dinge haben mich hier Zeit gekostet. Die Tools heißen `mcp__ahrefs__<toolname>`, nicht `ahrefs.<toolname>`, was zählt, sobald man in einem Prompt ein Werkzeug namentlich anspricht. Und eine laufende Sitzung lädt ihre MCP-Server ausschließlich beim Start, die eben hinzugefügte Verbindung erscheint also in der nächsten Sitzung, nicht in dieser. Ich habe dreimal neu gestartet in der Überzeugung, die Konfiguration sei falsch.

## Claude Desktop

Keine Konfigurationsdatei nötig. Einstellungen, dann Connectors, dann Add custom connector, dann die Endpunkt-URL einfügen. OAuth Client ID und Secret liegen unter den erweiterten Einstellungen, falls der Server sie braucht.

Ein architektonisches Detail überrascht dabei viele und entscheidet, was man überhaupt anbinden kann. Claude Desktop erreicht den MCP-Server nicht vom eigenen Rechner aus, sondern aus der Cloud-Infrastruktur von Anthropic. Bei einem gehosteten Dienst wie Ahrefs spielt das keine Rolle. Bei einem Server, der auf dem eigenen Laptop oder hinter einem Firmen-VPN läuft, macht es den ganzen Unterschied: Der muss erst aus dem öffentlichen Internet erreichbar sein, bevor überhaupt etwas funktioniert.

Kostenlose Konten sind auf einen eigenen Connector begrenzt, bezahlte nicht.

## Codex

Codex liest `~/.codex/config.toml` global, oder eine `.codex/config.toml` in einem Projektverzeichnis, das man als vertrauenswürdig markiert hat. Eine TOML-Tabelle pro Server, und der Transport ergibt sich daraus, welche Schlüssel gesetzt sind: Ein `command` bedeutet stdio, ein `url` bedeutet Streamable HTTP.

```toml
[mcp_servers.ahrefs]
url = "https://api.ahrefs.com/mcp/mcp"
bearer_token_env_var = "AHREFS_API_KEY"
```

Wichtig ist, was `bearer_token_env_var` erwartet. Dort steht der Name einer Umgebungsvariable, nicht das Token selbst. Wer den Schlüssel direkt hineinschreibt, bekommt eine Konfigurationsdatei voller Geheimnis und einen Server voller nichts.

Den Unterbefehl `codex mcp add` gibt es, er ist aber auf stdio-Server zugeschnitten. Für einen entfernten Endpunkt ist das direkte Bearbeiten der TOML schneller und lässt sich besser versionieren. Prüfen mit `codex mcp list`.

## Cursor, VS Code und Windsurf

Alle drei sprechen dasselbe JSON mit einem ärgerlichen Unterschied: dem Schlüssel, der die URL trägt. Cursor nennt ihn `url`. VS Code will `url` zusammen mit einem ausdrücklichen `"type": "http"`, dieselbe Form wie Claude Code. Windsurf nennt ihn `serverUrl`. Alles Übrige am Block wird unverändert übernommen, eine funktionierende Konfiguration ist also dreißig Sekunden Umbenennen von der nächsten entfernt.

VS Code unterstützt MCP nativ seit 1.99, bedient über Copilot Chat. Windsurf hat es Anfang des Jahres nachgezogen. Wenn ein Team über mehrere Editoren verteilt ist, lohnt es sich, den Block einmal zu schreiben und alle drei Varianten als Snippet abzulegen.

## Kopflos, ganz ohne Client

MCP ist ein Sitzungsprotokoll, kein einfacher REST-Aufruf. Man initialisiert, schickt eine Initialized-Benachrichtigung, und erst danach lässt sich ein Werkzeug aufrufen. Jede Anfrage danach trägt die Sitzungs-ID, die man zurückbekommen hat.

```bash
curl -sD hdr -X POST "$AHREFS_MCP_URL" \
  -H "Authorization: Bearer $AHREFS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
       "protocolVersion":"2025-06-18","capabilities":{},
       "clientInfo":{"name":"sm","version":"1"}}}'

SID=$(grep -i '^mcp-session-id:' hdr | awk '{print $2}' | tr -d '\r')

curl -s -X POST "$AHREFS_MCP_URL" \
  -H "Authorization: Bearer $AHREFS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2025-06-18" \
  -H "mcp-session-id: $SID" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
```

Drei Dinge daran macht man leicht falsch. Den zweiten Aufruf nicht weglassen: Die Sitzungs-ID allein schließt den Handshake nicht ab, und ein Server, der die Initialized-Benachrichtigung nie bekommen hat, weist Werkzeug-Aufrufe zurück. Der `Accept`-Header braucht beide Inhaltstypen, auch wenn man nie vorhat, einen Stream zu lesen. Und ab der Revision 2025-06-18 muss jede Anfrage nach der Initialisierung den Header `MCP-Protocol-Version` tragen, er gehört also an die Benachrichtigung und an jeden folgenden Werkzeug-Aufruf, neben `mcp-session-id`.

Das in ein kleines Shell-Skript zu packen lohnt sich, weil dieselben Daten damit auch Cronjobs und Agenten offenstehen, die gar keine Chat-Oberfläche haben. Meines liegt als [ahrefs-mcp-kit](https://github.com/studiomeyer-io/ahrefs-mcp-kit) öffentlich, falls du lieber mit etwas anfängst, das schon läuft. Eine Warnung aus Erfahrung: Wer in so einem Helfer den JSON-Parameter mit einem Bash-Default wie `${ARG:-{}}` vorbelegt, bekommt durch das Klammer-Matching in der Vorbelegung stillschweigend eine überzählige schließende Klammer und damit ungültiges JSON. Der Aufruf scheitert dann ohne Ausgabe und ohne Fehler. Die Variable gehört in eine eigene Zeile.

## Der Tarif bestimmt die Zeilengrenze, und genau das wird teuer

Diesen Abschnitt hätte ich gern zuerst gelesen, denn er erklärt einen Fehler, der mich an einem einzigen Vormittag ein Monatsbudget gekostet hat.

Ahrefs staffelt zwei Dinge nach Tarif: wie viele Units man im Monat bekommt, und wie viele Zeilen eine einzelne Anfrage zurückgeben darf.

| Tarif | Units pro Monat | Zeilen pro Anfrage |
|---|---:|---:|
| Lite | 100.000 | 100 |
| Standard | 400.000 | 250 |
| Advanced | 1.000.000 | 500 |
| Enterprise | 2.000.000 | unbegrenzt |

Diese Zahlen haben sich am 28. April 2026 geändert, und zwar deutlich. Lite ging von 25.000 auf 100.000 Units und von 10 auf 100 Zeilen. Standard von 150.000 auf 400.000 Units und von 25 auf 250 Zeilen. Advanced verdoppelte die Units und ging von 100 auf 500 Zeilen.

Jetzt daneben halten, wie abgerechnet wird. Ein Aufruf kostet mindestens 50 Units, darüber hinaus zahlt man pro Zeile, multipliziert mit der Anzahl der abgefragten Spalten. Aufschlagsspalten wie `volume`, `keyword_difficulty` und `traffic_domain` kosten je rund zehn Units zusätzlich pro Zeile.

Beides zusammen ergibt die Falle. Der teuerste Aufruf meines gesamten Protokolls lief mit einer Grenze von 250 Zeilen. Das ist keine Zahl, die ich nach Überlegung gewählt hätte. Es ist exakt die Zeilengrenze meines Tarifs, und ich habe danach gegriffen, weil sie das Maximum war. Jeder dieser Aufrufe kostete 5.250 Units. Dieselbe Abfrage mit 50 Zeilen hätte 1.050 gekostet und dasselbe ergeben, weil die Zeilen 51 bis 250 Rauschen waren, das ich nie benutzt habe.

Die Zeilengrenze ist keine Empfehlung. Sie ist eine Decke, und seit April liegt diese Decke je nach Tarif bis zu zehnmal höher als vorher. Für Code mit einem ausdrücklichen Grenzwert ist das harmlos, 50 bleibt 50. Zu spüren bekommt es, wer gar kein Limit übergibt, wer nach dem jeweils aktuellen Maximum greift, und wer eine Abfrage laufen hat, die früher an der alten Decke abgeschnitten wurde und jetzt die vollen zehnmal mehr Zeilen liefert. Alle drei Fälle sind in Skripten von vor dem Frühjahr üblich, und keiner sieht im Code anders aus.

Die Grenze sollte sich danach richten, was man tatsächlich liest. Für Keyword-Erweiterung beginne ich inzwischen bei 50 und gehe nur höher, wenn ein Ergebnis sichtbar an der Grenze abgeschnitten war und die zusätzlichen Zeilen etwas ändern.

## Zehn Fallen, die mich je einen Durchlauf gekostet haben

Keine davon steht so in der Dokumentation, dass man sie findet, bevor man hineinläuft. Alle haben entweder eine Fehlermeldung erzeugt, die entziffert werden wollte, oder schlimmer: ein leeres Ergebnis, das wie ein Befund aussah.

**`where` ist JSON, nie ein String-Ausdruck.** Wer `"position>3 and position<15"` schreibt, bekommt `bad where: invalid JSON syntax`. Derselbe Filter als JSON braucht eine Klausel je Bedingung:

```json
{"and":[{"field":"position","is":["gt",3]},
        {"field":"position","is":["lt",15]}]}
```

Operatoren sind `gt`, `gte`, `lt`, `lte`, `eq`.

**`select` wechselt den Typ zwischen den Werkzeugen.** Site Explorer und Keywords Explorer wollen einen kommagetrennten String. `batch-analysis` will ein Array. Verwechselt man das, kommt in der einen Richtung `column '["domain"' not found` und in der anderen `expected array but got string`.

**Umlaute nicht umschreiben.** Die Keyword-Suche vergleicht wörtlich. Ein deutscher Begriff mit `ue` statt `ü` liefert null Zeilen und sieht aus wie ein totes Keyword. Für spanische Akzente gilt dasselbe.

**Der Länderfilter kann die Wahrheit stillschweigend wegschneiden.** Eine Filterung organischer Keywords nach Land ergab null Zeilen für eine Domain, die nachweislich rankt, weil ihre Platzierungen in anderen Ländern lagen. Ich habe 50 Units für eine leere Antwort gezahlt und wäre fast zum Schluss gekommen, die Domain ranke für nichts. Besser ohne Filter ziehen und `keyword_country` als Spalte mitnehmen.

**Standardmäßig `mode: "subdomains"` verwenden.** Bei jeder Site, deren Apex auf www weiterleitet, misst `mode: "domain"` nur den exakten Host und liefert Phantom-Nullen. Ich habe eine Domain gesehen, die im Domain-Modus ein paar hundert Backlinks und keinen Verkehr meldete, im Subdomain-Modus aber zweiundzwanzigtausend Backlinks.

**`volume-history` erwartet `keyword`, im Singular.** Mit `keywords` scheitert der Aufruf an `required arguments [keyword] are missing`. Sechsundzwanzig Aufrufe in einem Durchgang, alle abgewiesen, alle wegen eines Buchstabens.

**Heute ist kein gültiges `date_to`.** Selbst die aggregierten Endpunkte weisen das aktuelle Datum mit `bad date_to` ab. Gestern nehmen.

**Ein Site-Audit-Problem nie über den Namen suchen.** Die Namen sind nicht eindeutig. Ich habe denselben Namen an zwei verschiedenen Problem-IDs gefunden, von denen eine dauerhaft leer war. Die ID gehört aus der eigenen Issues-Antwort genommen, nicht über einen Textabgleich.

**Leere Search-Console-Detailtabellen bedeuten keine kaputte Verbindung.** Die Detail-Endpunkte materialisieren mit rund sechs Wochen Verzögerung. Eine Abfrage über die letzten 30 Tage kommt also leer zurück, während die Aggregate bis gestern aktuell sind. Ich habe zwei Leute erlebt, die daraus schlossen, ihre Anbindung sei tot, während sie einwandfrei lief.

**`org_traffic` ist ein Modell, keine Messung.** Der Wert wird aus Keywords im Ahrefs-Index mal einer Klickkurve geschätzt. Was der Index nicht führt, existiert in dieser Zahl schlicht nicht. Auf den Nischenseiten, die ich gegen echte Search-Console-Daten gehalten habe, lag er weit zu niedrig, in einem Fall um einen Faktor, den ich ohne beide Zahlen nebeneinander nicht geglaubt hätte. Auf eigenen Domains ist die Search Console die Wahrheit. Bei fremden Domains gehört der Wert als Untergrenze gekennzeichnet, nicht als Zahl.

## Erst die kostenlosen Flächen, dann Geld ausgeben

Die beste Gewohnheit, die ich mitgenommen habe: Alles, was zu einem verifizierten eigenen Projekt gehört, ist kostenlos oder fast kostenlos, und fast niemand rührt es an.

Über mein gesamtes Protokoll kosteten die Search-Console-Endpunkte, die Verwaltungs-Endpunkte und die kostenlose Domain-Rating-Abfrage zusammen null Units und lieferten knapp 6.000 Zeilen. Es gibt genau eine Ausnahme davon, und sie ist eine Falle: `gsc-anonymous-queries` trägt dasselbe `gsc-`-Präfix, rechnet aber ab 50 Units pro Abruf ab und liefert auf einer kleinen Site fast nichts. `site-audit-page-explorer` kostet pauschal 50 Units pro Anfrage, unabhängig davon, wie viele Zeilen zurückkommen, was über knapp 10.000 Zeilen auf etwa ein Drittel Unit pro Zeile hinauslief, bei mehr als zwanzig technischen Feldern je URL. Rank Tracker und Abo-Informationen sind ebenfalls kostenlos.

Auf der anderen Seite der Rechnung: Drei Werkzeuge, allesamt zeilenbasierte Keyword- und Backlink-Abfragen, standen für 78 Prozent meiner gesamten Ausgaben. Die günstigen und kostenlosen Flächen lieferten rund 18.000 Zeilen für 3.400 Units. Die drei teuren lieferten 30.000 Zeilen für 592.000. Das ist rund Faktor hundert im Gegenwert bei gleichem Budget.

Die Reihenfolge lautet also: Restbudget prüfen, die kostenlosen Projektflächen ausschöpfen, mit den pauschal abgerechneten Audit-Werkzeugen in die Tiefe gehen, und erst dann zu den zeilenbasierten greifen, mit einer Grenze, die man bewusst gewählt hat.

## Was ich jemandem sagen würde, der morgen anfängt

Einen Schlüssel mit MCP-Scope erzeugen, den Client auf den gehosteten Endpunkt richten, nichts installieren. OAuth nehmen, wer im Chat arbeitet, Bearer, wenn etwas nach Zeitplan laufen soll. Vor der ersten echten Abfrage die Abo-Information aufrufen und die eigene Zeilengrenze aus dem Tarif ablesen, statt sie anzunehmen.

Und dann die eigenen Zahlen aufschreiben. Die Kosten in diesem Text sind das, was ich auf einem Tarif über 1.102 Abrufe gemessen habe, und das Preismodell hat sich im April spürbar bewegt. Jede Antwort trägt ihre tatsächlichen Kosten mit, das Werkzeug sagt einem also, was es berechnet hat, wenn man hinsieht. Zwei Tage dieses Feld mitzuschreiben hat mir mehr darüber beigebracht, wo das Geld hingeht, als jede Menge Dokumentation.

## Weiterlesen, Claude + Claude Code

- [MCP-Transporte: stdio gegen HTTP, und was sich gerade geändert hat](https://studiomeyer.io/de/blog/mcp-stdio-vs-http.md)
- [Dokumente mit Claude lesen und analysieren](https://studiomeyer.io/de/blog/analyze-documents-with-claude.md)
- [Die sich wiederholenden Teile deiner Woche mit Claude automatisieren](https://studiomeyer.io/de/blog/automating-tasks-with-claude.md)
- [Claude 2026: Modelle, Apps, Claude Code und die API](https://studiomeyer.io/de/blog/claude-guide-2026.md)
