Skip to main content
Glama
eddii1

Payment Delay MCP

by eddii1

Payment Delay MCP - Serving a Production ML Model to Any LLM Over MCP

A scikit-learn classifier deployed behind a FastAPI microservice and published to language models as Model Context Protocol tools - so an off-the-shelf chat client discovers and invokes the model correctly with zero integration code written against it.

gpt-oss-120b selects the right MCP tool and calls the model API

The thesis

The model is the payload, not the point.

Most "AI-powered" demos hardcode a model call into a bespoke application. This project inverts that: the classifier is published as a protocol, so the LLM client is interchangeable. The same server drives OpenWebUI in Docker, OpenCode on the CLI, and Claude Desktop - with no code change and no client-specific adapter.


Overview

A telecom operator wants to know which customers will pay late. A trained classifier answers that, but a .pkl file is not a product - somebody still has to write glue code to call it, and that glue is rewritten for every new consumer.

This repository is the glue, written once as a protocol. Four layers, each independently deployable:

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

The two paths are deliberately separate. The LLM never executes anything. It emits a tool_calls message naming a tool and its arguments; the client executes it and replays the result. That distinction is what makes the model swappable - and it is why this stack works identically whether the reasoning layer is Bedrock, a local Ollama, or Claude.


Related MCP server: Company API MCP Server

The core idea: tool selection is a documentation problem

An LLM picks a tool from its name, signature, and docstring - nothing else. No fine-tuning, no examples, no routing logic. So the docstrings are the interface, and writing them is engineering work, not commentary.

Two tools here overlap heavily. Both predict payment delay. Getting the model to choose correctly, unprompted, required encoding the operational constraints directly into the description:

Tool

When the model should pick it

The disambiguating signal

predict_payment_delay

User has a CSV, as a path or pasted text

Docstring warns that csv_path fails when the server runs in a container that cannot see the user's filesystem, and to prefer csv_text

predict_single_customer

User describes one customer in prose

Docstring says "for natural-language cases where the LLM extracts a single customer into structured features"

Verified result: given a customer described in plain English, gpt-oss-120b selected predict_single_customer over predict_payment_delay unaided, filled the feature dictionary from the prose, and returned a grounded answer. Confirmed in the logs of both hops - POST /predict_single_customer 200 at mcpo, then POST /predict 200 at the model service.

That is the whole claim of the project, and it is falsifiable: disable the tool and the same model answers the same question confidently and wrongly, with both log panes empty.


A request, end to end

The part most tool-use diagrams omit is that a single user question costs two round trips to the model, and the intermediate assistant message must be replayed verbatim or the tool_call_id dangles:

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

The tools[] array is re-sent on every request - the model is stateless and rediscovers the toolset each turn.


What is verified

Four checkpoints, each confirmed against logs rather than assumption:

#

Layer

Evidence

1

Model service

GET /health 200; single-row POST /predict returns no, confidence 0.985

2

mcpo bridge

5 tools rendered at :8001/docs; predict_single_customer executed by curl

3

LiteLLM to Bedrock

/v1/models lists the model; a tool-calling request returns finish_reason: tool_calls

4

Full autonomous loop

POST /predict_single_customer 200 at mcpo and POST /predict 200 at the API, from a plain-English question

Checkpoint 3 matters more than it looks: finish_reason: tool_calls is the only way to distinguish "the model declined to use the tool" from "the tool was never offered to it." Those failures look identical in the chat window.


The model

Dataset disclosure. The training data is a public telecom churn benchmark with the target column relabelled payment_delay for the purposes of this exercise. The features are call-record and account fields, not billing history. The modelling is real and the pipeline is real; the business framing is synthetic. Treat the numbers as a working example, not a validated credit-risk model.

Property

Value

Rows / columns

3,000 / 20

Class balance

no 2,587 (86.23%) · yes 413 (13.77%)

Pipeline

ColumnTransformer -> RandomOverSampler -> RandomForestClassifier (imblearn)

Split

80/20 stratified

Features at inference

36 - 19 raw plus 17 derived <column>_is_outlier flags

Decision threshold

0.35, persisted as an artifact

The threshold is not 0.5 and is not hardcoded. It ships as models/threshold.pkl and is overridable per request, because on a target that is 13.77% positive the default cutoff optimises for the wrong thing. A lower threshold catches more late payers at the cost of more false positives, and which trade-off is correct is a business decision, not a modelling one - so the API exposes it as a parameter.

Nothing in the codebase hardcodes a column name. Feature order comes from feature_columns.pkl, outlier bounds from outlier_bounds.pkl, so retraining does not require a code change.


Engineering decisions worth defending

The MCP server never imports the model. It calls the API over HTTP. That keeps the MCP process small - no sklearn, no 9 MB pickle resident - and lets the model service scale, deploy, and get monitored like any other microservice. A protocol adapter should hold no business logic.

Prediction runs off the event loop. The inference call is dispatched with run_in_threadpool, so CPU-bound scoring never blocks FastAPI's async loop under concurrent requests.

stdio discipline. MCP over stdio requires stdout to carry JSON-RPC frames and nothing else, so a stray print() corrupts the stream and kills the session. Consequently all logging is routed to stderr, httpx and httpcore are silenced, and launcher.py redirects uvicorn's output to a log file, waits for /health, and only then hands the client clean stdio.

Two entrypoints for two topologies. server.py is the container entrypoint, where the API is a separate service. launcher.py is the local entrypoint, which starts the API itself and waits for it - the right shape for a desktop MCP client that expects one process to own its dependencies.

A pin that documents a real incident. mcp>=1.2.0,<2.0: mcp 2.x renamed streamablehttp_client, and mcpo 0.0.20 still imports the old name, so mcpo crash-loops against 2.x. The cap is commented in requirements.txt with the reason, because a version pin without a reason is deleted by the next person who reads it.


Repository structure

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

Getting started

Run the model service alone - no cloud credentials needed

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

Endpoint

Purpose

GET /health

service up, model loaded

GET /model/info

model type, classes, features, threshold

GET /schema

required CSV columns

POST /predict

one row (JSON object or one-row CSV) -> one yes/no

POST /predict/batch

multi-row CSV -> one yes/no per row

POST /predict/summary

multi-row CSV -> one yes/no for the whole file

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

Attach your own MCP client

python3 -m payment_delay.mcp_server.launcher

Serves the tools over stdio and starts the API if it is not already healthy. opencode.json wires this into OpenCode; Claude Desktop and any other stdio MCP client attach the same way.

Run the full stack

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

Full runbook, including credential setup and troubleshooting: docs/docker-stack.md.


The MCP surface

Five tools, two resources, one prompt template:

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

Resources and prompts are the underused half of MCP. The context resource means the client does not need to be told what the model is for - it can read it.


Tech stack

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

Docs


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