Skip to main content
Glama
eddii1

Payment Delay MCP

by eddii1

Payment Delay MCP – Auslieferung eines Produktions-ML-Modells an beliebige LLMs über MCP

Ein scikit-learn-Klassifikator, der hinter einem FastAPI-Mikroservice bereitgestellt und Sprachmodellen als Model Context Protocol-Tools zur Verfügung gestellt wird – sodass ein handelsüblicher Chat-Client das Modell korrekt entdeckt und aufruft, ohne dass dafür auch nur eine Zeile Integrationscode geschrieben wurde.

gpt-oss-120b wählt das richtige MCP-Tool aus und ruft die Modell-API auf

Die These

Das Modell ist die Fracht, nicht der Zweck.

Die meisten „KI-gestützten“ Demos verdrahten einen Modellaufruf fest in eine maßgeschneiderte Anwendung. Dieses Projekt kehrt das um: Der Klassifikator wird als Protokoll veröffentlicht, sodass der LLM-Client austauschbar ist. Derselbe Server treibt OpenWebUI in Docker, OpenCode auf der CLI und Claude Desktop an – ohne Codeänderung und ohne client-spezifischen Adapter.


Überblick

Ein Telekommunikationsanbieter möchte wissen, welche Kunden ihre Rechnung zu spät bezahlen. Ein trainierter Klassifikator beantwortet das, aber eine .pkl-Datei ist kein Produkt – jemand muss trotzdem Klebecode schreiben, um sie aufzurufen, und dieser Klebecode wird für jeden neuen Konsumenten neu geschrieben.

Dieses Repository ist der Klebecode, einmal geschrieben als Protokoll. Vier Schichten, jede unabhängig bereitstellbar:

flowchart TB
    subgraph reasoning["Reasoning path"]
        UI["OpenWebUI<br/>:3000"] -->|OpenAI protocol| LL["LiteLLM<br/>:4000"]
        LL -->|bedrock_mantle| BR["AWS Bedrock<br/>gpt-oss-120b"]
    end

    subgraph tools["Tool path"]
        UI -->|OpenAPI| MCPO["mcpo<br/>:8001"]
        MCPO -->|MCP over stdio| FM["FastMCP server<br/>5 tools · 2 resources · 1 prompt"]
        FM -->|HTTP| API["FastAPI service<br/>:8000"]
        API --> PRED["inference.predictor<br/>the only code that<br/>opens the pickle"]
        PRED --> PKL[("models/*.pkl<br/>RandomForest +<br/>RandomOverSampler")]
    end

    style reasoning fill:#1f2a3710,stroke:#8884
    style tools fill:#1f372a10,stroke:#8884

Die beiden Pfade sind bewusst getrennt. Das LLM führt niemals etwas aus. Es sendet eine tool_calls-Nachricht, die ein Tool und dessen Argumente benennt; der Client führt sie aus und spielt das Ergebnis zurück. Diese Unterscheidung ist es, die das Modell austauschbar macht – und sie ist der Grund, warum dieser Stack identisch funktioniert, ob die Reasoning-Ebene nun Bedrock, ein lokales Ollama oder Claude ist.


Related MCP server: Company API MCP Server

Die Kernidee: Tool-Auswahl ist ein Dokumentationsproblem

Ein LLM wählt ein Tool anhand von Name, Signatur und Docstring aus – sonst nichts. Kein Fine-Tuning, keine Beispiele, keine Routing-Logik. Die Docstrings sind also die Schnittstelle, und das Schreiben derselben ist Ingenieursarbeit, kein Kommentar.

Zwei Tools hier überschneiden sich stark. Beide sagen Zahlungsverzug voraus. Damit das Modell ohne Aufforderung richtig wählt, mussten die operativen Randbedingungen direkt in die Beschreibung kodiert werden:

Tool

Wann das Modell es wählen soll

Das unterscheidende Signal

predict_payment_delay

Der Nutzer hat eine CSV, als Pfad oder eingefügten Text

Der Docstring warnt, dass csv_path fehlschlägt, wenn der Server in einem Container läuft, der das Dateisystem des Nutzers nicht sehen kann, und empfiehlt, csv_text zu bevorzugen

predict_single_customer

Der Nutzer beschreibt einen einzelnen Kunden in Prosa

Der Docstring sagt „für natürlichsprachliche Fälle, in denen das LLM einen einzelnen Kunden in strukturierte Features extrahiert“

Verifiziertes Ergebnis: Bei einem in einfachem Englisch beschriebenen Kunden wählte gpt-oss-120b ohne Unterstützung predict_single_customer statt predict_payment_delay, füllte das Feature-Wörterbuch aus der Prosa und lieferte eine fundierte Antwort. Bestätigt in den Logs beider Hops – POST /predict_single_customer 200 bei mcpo, dann POST /predict 200 beim Modellservice.

Das ist die gesamte Behauptung des Projekts, und sie ist falsifizierbar: Deaktiviert man das Tool, beantwortet dasselbe Modell dieselbe Frage selbstbewusst und falsch – bei beiden leeren Log-Bereichen.


Ein Request, Ende zu Ende

Der Teil, den die meisten Tool-Use-Diagramme auslassen: Eine einzige Nutzerfrage kostet zwei Roundtrips zum Modell, und die dazwischenliegende Assistant-Nachricht muss wörtlich wiedergegeben werden, sonst bleibt die tool_call_id in der Luft hängen:

sequenceDiagram
    participant U as User
    participant W as OpenWebUI
    participant L as LiteLLM
    participant M as Bedrock model
    participant O as mcpo
    participant S as FastMCP
    participant A as FastAPI + model

    U->>W: "Will customer X pay late?"
    W->>L: messages[] + tools[]
    L->>M: translated to Bedrock
    M-->>W: finish_reason: tool_calls
    Note over W: the client executes,<br/>not the model
    W->>O: POST /predict_single_customer
    O->>S: MCP call over stdio
    S->>A: POST /predict
    A-->>S: {prediction, probability_yes}
    S-->>O: result
    O-->>W: 200 OK
    W->>L: messages[] + assistant(tool_calls) + tool(result)
    L->>M: second round trip
    M-->>U: grounded natural-language answer

Das tools[]-Array wird bei jedem Request erneut gesendet – das Modell ist zustandslos und entdeckt das Toolset in jeder Runde neu.


Was verifiziert ist

Vier Prüfpunkte, jeder gegen Logs bestätigt statt gegen Annahmen:

#

Schicht

Beleg

1

Modellservice

GET /health 200; einzeiliger POST /predict liefert no, confidence 0.985

2

mcpo-Brücke

5 Tools gerendert unter :8001/docs; predict_single_customer per curl ausgeführt

3

LiteLLM zu Bedrock

/v1/models listet das Modell; eine Tool-Calling-Anfrage liefert finish_reason: tool_calls

4

Vollständige autonome Schleife

POST /predict_single_customer 200 bei mcpo und POST /predict 200 bei der API, ausgehend von einer Frage in einfachem Englisch

Prüfpunkt 3 ist wichtiger, als es aussieht: finish_reason: tool_calls ist der einzige Weg, um „das Modell hat die Tool-Nutzung abgelehnt“ von „das Tool wurde ihm nie angeboten“ zu unterscheiden. Diese Fehler sehen im Chat-Fenster identisch aus.


Das Modell

Offenlegung der Daten. Die Trainingsdaten sind ein öffentlicher Telekom-Churn-Benchmark, dessen Zielspalte für diese Übung in payment_delay umbenannt wurde. Die Features sind Anrufprotokoll- und Kontofelder, keine Abrechnungshistorie. Die Modellierung ist real und die Pipeline ist real; der geschäftliche Rahmen ist synthetisch. Behandeln Sie die Zahlen als funktionierendes Beispiel, nicht als validiertes Kreditrisikomodell.

Eigenschaft

Wert

Zeilen / Spalten

3.000 / 20

Klassenbalance

no 2.587 (86,23 %) · yes 413 (13,77 %)

Pipeline

ColumnTransformer -> RandomOverSampler -> RandomForestClassifier (imblearn)

Aufteilung

80/20 stratifiziert

Features bei der Inferenz

36 – 19 rohe plus 17 abgeleitete <column>_is_outlier-Flags

Entscheidungsschwelle

0,35, als Artefakt persistiert

Die Schwelle ist nicht 0,5 und nicht fest verdrahtet. Sie wird als models/threshold.pkl ausgeliefert und ist pro Request überschreibbar, denn bei einem Ziel, das zu 13,77 % positiv ist, optimiert der Standard-Cutoff auf das Falsche. Eine niedrigere Schwelle erfasst mehr Zahlungsverzögerer, um den Preis von mehr False Positives – und welcher Kompromiss richtig ist, ist eine Geschäftsentscheidung, keine Modellierungsentscheidung – deshalb stellt die API sie als Parameter bereit.

Nichts im Codebase verdrahtet einen Spaltennamen fest. Die Feature-Reihenfolge stammt aus feature_columns.pkl, die Ausreißergrenzen aus outlier_bounds.pkl, sodass ein Retraining keine Codeänderung erfordert.


Technische Entscheidungen, die eine Verteidigung wert sind

Der MCP-Server importiert das Modell nie. Er ruft die API über HTTP auf. Das hält den MCP-Prozess klein – kein sklearn, kein 9-MB-Pickle im Speicher – und erlaubt es dem Modellservice, wie jeder andere Mikroservice skaliert, bereitgestellt und überwacht zu werden. Ein Protokolladapter sollte keine Geschäftslogik enthalten.

Die Vorhersage läuft außerhalb der Event-Loop. Der Inferenzaufruf wird mit run_in_threadpool ausgeführt, sodass CPU-gebundenes Scoring FastAPIs asynchrone Loop unter gleichzeitigen Requests nie blockiert.

stdio-Disziplin. MCP über stdio erfordert, dass stdout JSON-RPC-Frames und sonst nichts trägt – ein verirrtes print() korrumpiert den Stream und beendet die Sitzung. Folglich wird das gesamte Logging auf stderr umgeleitet, httpx und httpcore werden stummgeschaltet, und launcher.py leitet uvicorns Ausgabe in eine Logdatei um, wartet auf /health und übergibt dem Client erst dann sauberes stdio.

Zwei Einstiegspunkte für zwei Topologien. server.py ist der Container-Einstiegspunkt, bei dem die API ein separater Service ist. launcher.py ist der lokale Einstiegspunkt, der die API selbst startet und auf sie wartet – die richtige Form für einen Desktop-MCP-Client, der erwartet, dass ein Prozess seine Abhängigkeiten besitzt.

Ein Pin, der einen realen Vorfall dokumentiert. mcp>=1.2.0,<2.0: mcp 2.x hat streamablehttp_client umbenannt, und mcpo 0.0.20 importiert noch den alten Namen, sodass mcpo gegen 2.x in eine Crash-Loop gerät. Die Obergrenze ist in requirements.txt mit dem Grund kommentiert, denn ein Versions-Pin ohne Grund wird von der nächsten Person, die ihn liest, gelöscht.


Repository-Struktur

mcp-payment-delay/
├── src/payment_delay/
│   ├── config.py                 # single source of truth for paths + endpoints, all env-overridable
│   ├── inference/predictor.py    # the only code that opens the pickle; imports no web framework
│   ├── api/main.py               # thin FastAPI adapter over the predictor
│   └── mcp_server/
│       ├── server.py             # FastMCP tools, resources, prompt (container entrypoint)
│       ├── api_client.py         # HTTP calls into the model service
│       └── launcher.py           # starts the API, then serves MCP on clean stdio (local entrypoint)
├── models/                       # model, threshold, outlier bounds, feature order
├── data/telecomunicatii.csv      # sample dataset
├── deploy/litellm_config.yaml    # Bedrock routing
├── scripts/bedrock_smoke_test.py # asserts a tool call comes back, not merely a 200
├── docs/                         # architecture + Docker runbook
├── Dockerfile                    # one image, serves both the API and the mcpo bridge
└── docker-compose.yml            # API + mcpo + LiteLLM + OpenWebUI

Erste Schritte

Nur den Modellservice ausführen – keine Cloud-Anmeldedaten nötig

python3 -m venv .venv && source .venv/bin/activate
make install                       # pip install -e ".[dev]"
make api                           # http://localhost:8000/docs

Endpoint

Zweck

GET /health

Service läuft, Modell geladen

GET /model/info

Modelltyp, Klassen, Features, Schwelle

GET /schema

Erforderliche CSV-Spalten

POST /predict

eine Zeile (JSON-Objekt oder einzeilige CSV) -> ein ja/nein

POST /predict/batch

mehrzeilige CSV -> ein ja/nein pro Zeile

POST /predict/summary

mehrzeilige CSV -> ein ja/nein für die gesamte Datei

curl -F "file=@data/telecomunicatii.csv" \
     "http://localhost:8000/predict/summary?threshold=0.35"

Einen eigenen MCP-Client anbinden

python3 -m payment_delay.mcp_server.launcher

Stellt die Tools über stdio bereit und startet die API, falls sie noch nicht gesund ist. opencode.json verdrahtet dies in OpenCode; Claude Desktop und jeder andere stdio-MCP-Client binden sich auf dieselbe Weise an.

Den vollständigen Stack ausführen

cp .env.example .env               # add your Bedrock key
python3 scripts/bedrock_smoke_test.py
make stack                         # http://localhost:3000

Vollständiges Runbook, einschließlich Einrichtung der Anmeldedaten und Fehlerbehebung: docs/docker-stack.md.


Die MCP-Oberfläche

Fünf Tools, zwei Ressourcen, eine Prompt-Vorlage:

get_api_health           service + model status
get_model_info           model metadata, classes, features, endpoints
get_input_schema         expected CSV columns
predict_payment_delay    CSV in (path or text), per-row or aggregate, threshold configurable
predict_single_customer  one customer as a JSON object

payment-delay://context       business + modelling context, injected as a resource
payment-delay://api-contract  the HTTP contract these tools call

interpret_payment_delay_result   prompt template for business-language explanation

Ressourcen und Prompts sind die untergenutzte Hälfte von MCP. Die Kontextressource bedeutet, dass dem Client nicht gesagt werden muss, wofür das Modell da ist – er kann es nachlesen.


Technologie-Stack

FastAPI · FastMCP · mcpo · scikit-learn · imbalanced-learn · pandas · LiteLLM · AWS Bedrock · OpenWebUI · Docker Compose · uvicorn · httpx

Dokumentation


Eduard-Gabriel Tudoran, 2026.

Maintenance

ActivityMaintained
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Exposes enterprise KPIs, health scores, forecasting, and anomaly detection as MCP tools, resources, and prompts for use by any MCP-compatible agent.
    2
    AGPL 3.0
  • F
    license
    Not graded
    quality
    C
    maintenance
    Exposes internal company services as LLM-callable MCP tools, enabling AI agents to perform business operations like customer management, order processing, and support ticketing through natural language.
  • F
    license
    Not graded
    quality
    C
    maintenance
    Exposes a governed lending portfolio (loans, customers, risk-tier history) to any MCP-compatible AI client via read-only tools, schema resources, and analysis prompts, wrapping an existing API gateway instead of connecting directly to the database.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eddii1/mcp-payment-delay'

If you have feedback or need assistance with the MCP directory API, please join our Discord server