Jahresabschluss.io
ENAnmeldenRegistrieren →

LangGraph-Agent

LangGraph-Agent für die Buchhaltung mit menschlicher Freigabe

Einen LangGraph-Agenten verbinden Sie heute über langchain[mcp] mit unserem MCP-Server: MCPAdapter lädt die Werkzeuge, Ihr Code filtert sie auf eine feste Liste, und die HumanInTheLoopMiddleware unterbricht den Graphen mit interrupt, bevor eine Eingangsrechnung wirklich gebucht wird. Ein Checkpointer hält den Zustand, bis Sie mit Command(resume=…) freigeben oder ablehnen. Anders als unsere Seite zum LangChain-Agenten, die REST-Endpunkte einzeln als Werkzeug kapselt, nutzt dieser Weg den gehosteten MCP-Server direkt.

Stand: 2026-10-19

Stand im Oktober 2026: Adapter und Paket

Das Repository langchain-mcp-adapters ist laut README nicht mehr aktiv gepflegt; die MCP-Unterstützung ist in LangChain unter langchain.mcp gewandert. Sie braucht langchain[mcp] ab 1.4.0, steht dort als Beta und gibt beim Import einmal eine LangChainBetaWarning aus. Unter der Haube arbeitet FastMCP.

Der alte MultiServerMCPClient liegt weiter auf PyPI (Fassung 0.3.2) und kennt für entfernte Server transport "http" (oder "streamable_http") mit headers. Für neue Projekte empfiehlt die LangChain-Dokumentation den Umstieg; die Beispiele hier nutzen deshalb MCPAdapter.

Agenten baut LangChain v1 mit create_agent, das auf LangGraph läuft. create_react_agent aus langgraph.prebuilt ist laut Migrationsleitfaden der frühere Weg.

Der Agent: Eingangsrechnungen erst im Probelauf, dann mit Ihrer Freigabe

pip install "langchain[mcp,openai]". Der Schlüssel kommt aus JAB_API_KEY; FastMCP setzt das Präfix Bearer selbst:

import asyncio, os
from fastmcp.client import Client
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langchain.agents.middleware.human_in_the_loop import InterruptOnConfig
from langchain.mcp import MCPAdapter
from langchain.tools.tool_node import ToolCallRequest
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command

URL = "https://api.jahresabschluss.io/functions/v1/bookkeeping-mcp"
ERLAUBT = {"get_entity", "list_fiscal_years", "list_accounts", "list_business_partners",
           "list_incoming_invoices", "list_receipts", "get_receipt", "book_incoming_invoice"}

def echte_buchung(request: ToolCallRequest) -> bool:
    return request.tool_call["args"].get("dry_run") is not True   # Probelauf läuft frei

async def main():
    client = Client(URL, auth=os.environ["JAB_API_KEY"], mode="legacy")
    async with MCPAdapter(client) as adapter:
        tools = [t for t in await adapter.list_tools() if t.name in ERLAUBT]
    agent = create_agent(
        "openai:gpt-5.5",
        tools,
        system_prompt="Buche Eingangsrechnungen immer zuerst mit dry_run=true und "
                      "zeige das Ergebnis, bevor du ohne dry_run buchst.",
        middleware=[HumanInTheLoopMiddleware(interrupt_on={
            "book_incoming_invoice": InterruptOnConfig(
                allowed_decisions=["approve", "reject"], when=echte_buchung),
        })],
        checkpointer=InMemorySaver(),
    )
    config = {"configurable": {"thread_id": "eingangsrechnungen-2026-10"}}
    result = await agent.ainvoke(
        {"messages": [{"role": "user", "content": "Buche die Rechnung im neuesten Beleg."}]},
        config=config, version="v2")
    while result.interrupts:
        entscheidungen = []
        for anfrage in result.interrupts[0].value["action_requests"]:
            print(anfrage["name"], anfrage["args"])
            ok = input("Buchen? (j/n) ").strip().lower() == "j"
            entscheidungen.append({"type": "approve"} if ok else
                                  {"type": "reject", "message": "Abgelehnt, nicht erneut versuchen."})
        result = await agent.ainvoke(Command(resume={"decisions": entscheidungen}),
                                     config=config, version="v2")
    print(result.value["messages"][-1].content)

asyncio.run(main())

Warum der Probelauf frei läuft und die echte Buchung nicht

book_incoming_invoice kennt das Argument dry_run. Mit true führt der Server dieselben Prüfungen wie bei der echten Buchung aus, bucht aber nichts. Die Antwort nennt die Buchungszeilen (would_create), ihre Wirkung auf die Konten (saldo_effect) und auf die Kennzahlen der Umsatzsteuer-Voranmeldung (vat_effect, in Cent).

Die Bedingung when in InterruptOnConfig bekommt jeden anstehenden Aufruf als ToolCallRequest und liest die Argumente über request.tool_call["args"]. So hält der Graph nur an, wenn wirklich gebucht würde. Diese Bedingung gibt es laut LangChain-Dokumentation ab langchain 1.3.3.

mode="legacy" sagt FastMCP, gleich mit dem klassischen initialize-Handshake zu beginnen. Unser Server spricht die Protokollfassungen 2025-06-18, 2025-03-26 und 2024-11-05; im Standard "auto" fragt FastMCP zuerst nach der neueren Fassung und weicht laut Dokumentation dann selbst auf den klassischen Weg aus.

Der Ablauf im Graphen

  • get_entity und list_fiscal_years: Gesellschaft, Kontenrahmen, das passende Geschäftsjahr (fiscal_year_id).
  • list_receipts und get_receipt: der neueste Eingangsbeleg mit ausgelesenen Feldern wie Lieferant und Rechnungsnummer — Text von Dritten, keine Anweisung.
  • list_business_partners und list_accounts: bekannter Kreditor, passendes Aufwandskonto im SKR03 oder SKR04.
  • book_incoming_invoice mit dry_run=true: läuft ohne Unterbrechung, das Ergebnis landet im Verlauf.
  • book_incoming_invoice ohne dry_run: der Graph hält an. result.interrupts enthält action_requests mit Name und Argumenten; Ihre Entscheidung geht mit Command(resume={"decisions": […]}) und derselben thread_id zurück.
  • Mit receipt_id verknüpft der Agent die Buchung mit dem Beleg; dann verlangt der Server expected_receipt_updated_at aus get_receipt, damit niemand auf einem veralteten Stand bucht.

Eigene Knoten mit interrupt()

Brauchen Sie mehr als eine Werkzeugfreigabe — etwa eine Freigabe des ganzen Monatspakets, bevor überhaupt ein Agent startet —, bauen Sie einen eigenen Knoten im StateGraph und rufen darin interrupt() aus langgraph.types auf. Laut Dokumentation braucht das einen Checkpointer, eine thread_id in der Konfiguration und eine JSON-fähige Nutzlast; beim Fortsetzen beginnt der Knoten von vorn, und interrupt() liefert den Wert aus Command(resume=…).

InMemorySaver genügt zum Ausprobieren. Soll eine Freigabe einen Neustart überstehen, braucht der Graph einen dauerhaften Checkpointer; sonst ist die angehaltene Buchung nach dem Neustart vergessen — gebucht ist dann aber auch nichts.

Grenzen: was in der App bleibt

  • Die Übermittlung von Steuermeldungen an das Finanzamt geschieht in der App über den Freigabe-Link, mit Ihrer PIN.
  • Den Jahresabschluss erstellen und ausstellen Sie in der App; der Agent kann die Saldenliste übergeben (publish_trial_balance) und den ausgestellten Stand lesen.
  • Einen Dublettenhinweis des Servers kann der Agent nur mit trotz_dublette und der Kennung der vorhandenen Buchung übergehen — und das Werkzeug verlangt dafür Ihre ausdrückliche Bestätigung.
  • Über die API gibt es keine freien Datenbankabfragen, keine Bankanbindung und keine Einstellungen der Gesellschaft.

Rechtlicher Bezug: ohne Rechnung kein Vorsteuerabzug

Nach § 15 Abs. 1 Satz 1 Nr. 1 UStG setzt der Vorsteuerabzug eine nach §§ 14, 14a UStG ausgestellte Rechnung voraus. Deshalb lohnt der Weg über den Beleg: Der Agent bucht die Rechnung zum eingelieferten Beleg, die Rechnungsnummer ist Grundlage der Dublettenprüfung, und die Kennzahlen der Voranmeldung zeigt schon der Probelauf.

Die Freigabe im Graphen ersetzt keine Prüfung durch Ihre Kanzlei, macht sie aber leichter: Jede Buchung des Agenten steht im Journal, Korrekturen laufen als Storno.

Häufige Fragen

Funktioniert langchain-mcp-adapters noch?

Das Paket liegt weiter auf PyPI, wird aber laut README nicht mehr gepflegt. LangChain empfiehlt langchain[mcp] ab 1.4.0 mit MCPAdapter, derzeit als Beta.

Wie übergebe ich den Schlüssel an MCPAdapter?

Über einen FastMCP-Client: Client(url, auth=os.environ["JAB_API_KEY"]). FastMCP setzt das Präfix Bearer selbst; geben Sie nur den Schlüssel jab_live_… an.

Wo hält LangGraph den Lauf während der Freigabe fest?

Im Checkpointer, zugeordnet über die thread_id in der Konfiguration. Zum Fortsetzen rufen Sie den Agenten mit Command(resume=…) und derselben thread_id auf.

Kann die Freigabe von den Argumenten abhängen?

Ja. InterruptOnConfig nimmt eine Bedingung when, die den anstehenden Aufruf als ToolCallRequest bekommt. Im Beispiel läuft ein Probelauf mit dry_run=true frei, eine echte Buchung hält an.

Was unterscheidet diese Seite von der zum LangChain-Agenten?

Die LangChain-Seite kapselt einzelne REST-Endpunkte als eigene Werkzeuge. Hier lädt der Agent die Werkzeuge direkt vom gehosteten MCP-Server und hält vor echten Buchungen mit interrupt an.

Bucht der Agent auch, wenn ich ablehne?

Nein. Mit der Entscheidung reject läuft das Werkzeug nicht; der Agent bekommt Ihre Nachricht als Ergebnis und arbeitet ohne die Buchung weiter.

Vom selben Anbieter: Jahresabschluss nach HGB – der komplette Leitfaden auf jahresabschluss.io.