Jahresabschluss.io
ENAnmeldenRegistrieren →

Vercel AI SDK

Vercel AI SDK: Buchhaltung per MCP in TypeScript und Next.js

Im Vercel AI SDK verbinden Sie unseren MCP-Server mit createMCPClient aus @ai-sdk/mcp und dem Transport type 'http' samt Authorization-Kopfzeile; mcpClient.tools() liefert die Werkzeuge, die Sie auf eine feste Liste filtern, und toolApproval in generateText oder streamText verlangt Ihre Zustimmung, bevor ein schreibendes Werkzeug läuft. Diese Seite zeigt das mit der Fassung 7 des SDK an einer Ausgangsrechnung: Der Agent liest Kunde und offene Posten frei, create_outgoing_invoice läuft erst nach Ihrem Ja. Der Schlüssel bleibt dabei auf dem Server, nie im Browser.

Stand: 2026-10-19

Pakete und Fassung

  • npm install ai @ai-sdk/mcp @ai-sdk/openai — im Oktober 2026 ist ai in der Hauptfassung 7. Die Dokumentation zur Fassung 5 importierte noch experimental_createMCPClient; die aktuelle nutzt createMCPClient aus @ai-sdk/mcp.
  • Das Anbieterpaket ist austauschbar. Das Beispiel nutzt @ai-sdk/openai mit dem Modell 'gpt-6-astra' aus der Anbieterdoku; der Anbieterschlüssel kommt aus der Umgebungsvariable des Pakets.
  • toolApproval ersetzt das ältere needsApproval an einzelnen Werkzeugen; laut Dokumentation ist needsApproval veraltet, funktioniert aber noch.
  • stopWhen: isStepCount(n) begrenzt die Zahl der Schritte, in denen das Modell Werkzeuge aufruft. Laut Migrationsleitfaden zur Fassung 7 hieß die Funktion vorher stepCountIs.

Das Programm: lesen frei, Rechnung erst nach Freigabe

Ein Node-Skript auf dem Server; in einer Next.js-Route sieht der Teil mit createMCPClient genauso aus:

import { createMCPClient } from '@ai-sdk/mcp';
import { openai } from '@ai-sdk/openai';
import { generateText, isStepCount, type ModelMessage, type ToolApprovalResponse } from 'ai';
import { createInterface } from 'node:readline/promises';

const LESEN = new Set(['get_entity', 'list_fiscal_years', 'list_business_partners',
  'get_business_partner', 'list_outgoing_invoices', 'list_open_items']);
const SCHREIBEN = new Set(['create_outgoing_invoice']);

const mcp = await createMCPClient({
  transport: {
    type: 'http',
    url: 'https://api.jahresabschluss.io/functions/v1/bookkeeping-mcp',
    headers: { Authorization: `Bearer ${process.env.JAB_API_KEY}` },
  },
});
const rl = createInterface({ input: process.stdin, output: process.stdout });

try {
  const alle = await mcp.tools();
  const tools = Object.fromEntries(
    Object.entries(alle).filter(([name]) => LESEN.has(name) || SCHREIBEN.has(name)),
  );
  const messages: ModelMessage[] = [{ role: 'user', content:
    'Stelle dem Geschäftspartner <Name> eine Rechnung über 1.200 Euro netto für Beratung im Oktober 2026.' }];

  for (;;) {
    const result = await generateText({
      model: openai('gpt-6-astra'),
      tools,
      toolApproval: ({ toolCall }) =>
        SCHREIBEN.has(toolCall.toolName) ? 'user-approval' : 'not-applicable',
      stopWhen: isStepCount(8),
      messages,
    });
    messages.push(...result.responseMessages);

    const antworten: ToolApprovalResponse[] = [];
    for (const part of result.content) {
      if (part.type === 'tool-approval-request' && !part.isAutomatic) {
        console.log(part.toolCall.toolName, JSON.stringify(part.toolCall.input, null, 2));
        const ok = (await rl.question('Rechnung so ausstellen? (j/n) ')).trim() === 'j';
        antworten.push({ type: 'tool-approval-response', approvalId: part.approvalId, approved: ok });
      }
    }
    if (antworten.length === 0) { console.log(result.text); break; }
    messages.push({ role: 'tool', content: antworten });
  }
} finally {
  rl.close();
  await mcp.close();
}

So läuft die Freigabe ab

generateText hält bei einer Freigabe nicht an, sondern endet und liefert Teile vom Typ tool-approval-request in result.content. Ihr Code zeigt Werkzeug und Argumente, sammelt die Entscheidung als tool-approval-response mit der approvalId und ruft generateText mit den ergänzten Nachrichten erneut auf. Erst dann läuft create_outgoing_invoice — oder das Modell erfährt, dass Sie abgelehnt haben.

Der Server verlangt das ohnehin: Seine Anweisung an jeden Agenten lautet, vor create_outgoing_invoice Empfänger und Betrag zu zeigen und erst nach ausdrücklicher Bestätigung aufzurufen. Die Rechnung erhält eine Rechnungsnummer und lässt sich danach nur noch stornieren; die Werkzeugbeschreibung trägt den Hinweis destructiveHint.

In Next.js mit useChat erscheint derselbe Schritt als Teil im Zustand approval-requested; die Antwort schicken Sie mit addToolApprovalResponse({ id, approved }) zurück. Den MCP-Client und den Schlüssel halten Sie in der Server-Route, nie in Code, der im Browser landet.

Hinweise des Servers statt Namensliste?

Das SDK reicht die Hinweise des Servers (readOnlyHint, destructiveHint) an jedem Werkzeug unter metadata.annotations weiter, und die Dokumentation zeigt eine Richtlinie, die nur ausdrücklich lesende Werkzeuge ohne Freigabe laufen lässt. Sie warnt zugleich: Solche Hinweise sind Angaben des Servers und ersetzen keine feste Werkzeugliste, keine eingeschränkten Schlüssel und keine eigene Freigaberegel. Unser Server setzt readOnlyHint bei allen lesenden Werkzeugen; die Namensliste im Beispiel bleibt trotzdem die strengere Wahl.

Für Werkzeuge, die über die Responses-Schnittstelle direkt beim Modellanbieter laufen (openai.tools.mcp), gilt toolApproval laut Dokumentation nicht — sie werden beim Anbieter ausgeführt. Für eine Freigabe in Ihrer Anwendung ist der hier gezeigte Weg über createMCPClient der richtige.

Wiederholungen und Werkzeugänderungen

  • maxRetries an createMCPClient wiederholt tools/call bei vorübergehenden Fehlern. Die Dokumentation rät, das nur für Werkzeuge einzuschalten, deren Wiederholung gefahrlos ist. Unsere schreibenden Werkzeuge verlangen eine UUID als idempotency_key: Dieselbe UUID liefert bei einer Wiederholung dasselbe Ergebnis, eine neue erzeugt einen zweiten Vorgang.
  • fingerprintTools und detectToolDrift aus ai halten fest, welche Werkzeugbeschreibungen Sie geprüft haben, und melden spätere Änderungen. Für eine Buchhaltung ist das ein sinnvoller Zusatz zur festen Liste.

Grenzen: was in der App bleibt

  • Den Versand per E-Mail (send_outgoing_invoice) erlaubt nur ein Schlüssel mit dem eigenen Recht send, und er geht nur an hinterlegte Adressen des Geschäftspartners.
  • Steuermeldungen übermitteln Sie in der App über den Freigabe-Link; der Agent prüft und bereitet höchstens vor.
  • Einstellungen der Gesellschaft, Bankanbindung und Bankverbindungen der Geschäftspartner gibt es über die API nicht.
  • Belegdateien und Exporte im Kanzlei-Format gehen nicht durch das Modell; die Werkzeuge nennen den Download-Weg per REST.

Rechtlicher Bezug: die Rechnungsnummer

Nach § 14 Abs. 4 Nr. 4 UStG braucht eine Rechnung eine fortlaufende Nummer, die zur Identifizierung einmalig vergeben wird. Genau deshalb ist create_outgoing_invoice nicht rückholbar: Ist die Nummer vergeben, korrigieren Sie mit cancel_outgoing_invoice (Stornorechnung mit eigener Nummer) oder create_outgoing_credit_note, nie durch Löschen. Die Freigabe vor dem Aufruf ist der günstigste Zeitpunkt für Ihre Prüfung.

Häufige Fragen

Heißt es createMCPClient oder experimental_createMCPClient?

Die aktuelle Dokumentation des AI SDK nutzt createMCPClient aus dem Paket @ai-sdk/mcp. experimental_createMCPClient stammt aus älteren Fassungen, etwa der Dokumentation zur Fassung 5.

Wie gebe ich den Schlüssel mit?

Im Transport: { type: 'http', url, headers: { Authorization: `Bearer ${process.env.JAB_API_KEY}` } }. Der Schlüssel bleibt im Serverprozess.

Wie wähle ich nur bestimmte Werkzeuge?

Filtern Sie das Ergebnis von mcp.tools() nach Namen, wie im Beispiel. Alternativ lädt tools({ schemas }) nur die Werkzeuge, deren Schemas Sie selbst angeben.

Wie verlange ich eine Bestätigung vor einem Werkzeug?

Mit toolApproval in generateText oder streamText. Gibt die Funktion 'user-approval' zurück, endet der Lauf mit einem tool-approval-request, den Sie mit einer tool-approval-response beantworten.

Kann ich den MCP-Client im Browser verwenden?

Davon raten wir ab. Der Schlüssel öffnet die Bücher einer Gesellschaft; er gehört in eine Server-Route oder einen Serverprozess, nicht in Code, der an den Browser ausgeliefert wird.

Welches Modell muss ich nehmen?

Jedes Modell, das Werkzeugaufrufe beherrscht und für das es ein Anbieterpaket gibt. Das Beispiel nutzt @ai-sdk/openai; der Rest des Codes bleibt beim Wechsel gleich.

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