SM

shivamsingh2611-maker/mcp

Developer tools
0 stars 0 forks Quality 57 Trend 8

A runnable MCP server that turns the static prototype into a real journey you can drive by talking to Claude, Gemini or ChatGPT.

Overview

A runnable MCP server that turns the static prototype into a real journey you can drive by talking to Claude, Gemini or ChatGPT. Nothing here touches a Telekom system. Every tool answers from the markdown files in catalogue/, so the offer can be changed by editing a file and restarting — no code, no deployment, no engineer. The HTTP endpoint is http://127.0.0.1:8000/mcp. To reach it from a hosted assistant temporarily, put a tunnel in front: Use the https://….trycloudflare.com/mcp URL it prints. This is fine for a live demo but the URL is random and dies when your machine or the tunnel process stops. Cart state lives in memory in a single Python process (see CARTS in server.py), so this needs one persistent instance, not an autoscaled or serverless one. Render's free web service tier fits: one always-running container, a stable HTTPS domain, no card required. This repo already includes render.yaml and .python-version (mcp>=2.2.0 requires Python 3.10+, pinned here to 3.11.9). 1.

README

Telekom agentic commerce — demo MCP server

A runnable MCP server that turns the static prototype into a real journey you can drive by talking to Claude, Gemini or ChatGPT.

Nothing here touches a Telekom system. Every tool answers from the markdown files in catalogue/, so the offer can be changed by editing a file and restarting — no code, no deployment, no engineer.

server.py              the whole server: 9 tools, 3 prompts, 8 UI widgets
catalogue/tariffs.md   11 tariffs across mobile, fixed and TV/OTT
catalogue/devices.md   6 devices, priced per tariff
catalogue/consents.md  TKG 54, AGB, Widerruf, card mandate, and per-line-type extras

Run it

python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

python server.py                      # stdio, for a local desktop client
python server.py --http --port 8000   # remote, for a custom connector
python server.py --http --no-mcp-ui   # ...without the in-band ui:// widget

The HTTP endpoint is http://127.0.0.1:8000/mcp.

To reach it from a hosted assistant temporarily, put a tunnel in front:

npx cloudflared tunnel --url http://localhost:8000

Use the https://….trycloudflare.com/mcp URL it prints. This is fine for a live demo but the URL is random and dies when your machine or the tunnel process stops.

Deploy for real (Render)

Cart state lives in memory in a single Python process (see CARTS in server.py), so this needs one persistent instance, not an autoscaled or serverless one. Render’s free web service tier fits: one always-running container, a stable HTTPS domain, no card required.

This repo already includes render.yaml and .python-version (mcp>=2.2.0 requires Python 3.10+, pinned here to 3.11.9).

  1. Push this folder to a new GitHub repo.
  2. On render.com, New + → Blueprint, connect the repo. Render reads render.yaml and creates the web service automatically (build: pip install -r requirements.txt; start: python server.py --http --host 0.0.0.0 --port $PORT).
    • No render.yaml/Blueprint access? Use New + → Web Service instead and paste the same build/start commands by hand.
  3. Deploy. Render gives you https://.onrender.com.
  4. Your MCP endpoint is https://.onrender.com/mcp.

Verify it’s alive:

curl -i https://.onrender.com/mcp

(A 4xx/406 response is expected for a plain GET — the endpoint only speaks JSON-RPC over POST. Anything other than a connection error or 502 means the process is up.)

Then connect it: in Claude, Settings → Connectors → Add custom connector, paste that /mcp URL.

Free-tier caveats: the service spins down after ~15 minutes idle and takes 30–60s to wake on the next request (the first tool call in a new session may time out or feel slow — retry it). Every restart or redeploy also wipes in-memory carts, which is expected for this demo. If you need it always warm, upgrade the Render service to a paid instance (still the same render.yaml).

Connect it

Claude — Settings → Connectors → Add custom connector → paste the /mcp URL. Claude and Claude Desktop both support MCP Apps, so the widgets render inline. After changing a widget, remove and re-add the connector: hosts cache ui:// resources and will otherwise keep serving the old view.

ChatGPT — Settings → Connectors → Create, same URL. Tools work; widget rendering depends on Apps SDK support in your workspace.

Gemini — add as an MCP server in the Gemini CLI or any UCP/MCP-capable client.

Any client — npx @mcpjam/inspector@latest and point it at the URL. Best way to watch the raw JSON-RPC while you demo, and the easiest place to see a widget actually painted.

Where the UI renders

The server emits its widgets two different ways, because no single mechanism renders everywhere today:

Mechanism How it travels Renders in
MCP Apps (SEP-1865) _meta.ui.resourceUri on the tool + a separate ui:// resource the host fetches and renders in a sandboxed iframe Claude, Claude Desktop, VS Code Copilot, M365 Copilot, Goose, Postman, MCPJam, Archestra
MCP-UI (in-band) an EmbeddedResource with a ui:// URI and text/html, inside the tool result’s own content MCP-UI-aware clients and custom web chat
display_markdown a plain field in every tool result Everywhere, including voice

The MCP Apps lifecycle is strict and easy to get wrong. The view MUST send ui/initialize with protocolVersion, appCapabilities and clientInfo, and MUST then send ui/notifications/initialized — until the host sees that notification it is forbidden from sending anything back, so ui/notifications/tool-result never arrives and the view renders empty. That bug is what kept every widget here blank; see BRIDGE in server.py.

The in-band copy is a complete, standalone HTML document with that call’s data already baked in, so it needs no postMessage bridge from the host. Disable it with --no-mcp-ui or TELEKOM_MCP_UI=0 if a client dumps the raw HTML into the transcript instead of rendering it.

Verify what a client is actually getting:

curl -s -X POST https://.onrender.com/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -H 'mcp-session-id: ' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_tariffs","arguments":{"line_type":"mobile"}}}'

The display contract

Every tool returns two extra fields, and they are what keeps pricing under Telekom’s control rather than the model’s:

  • display_markdown — the priced table as Telekom words it, rendered server-side, including the minimum-term and post-term footnotes.
  • display_note — what the model should do with it, decided per request: when the client negotiated MCP Apps, “the widget shows this, don’t restate prices”; otherwise “show display_markdown verbatim”.

Tool descriptions and the server instructions carry the price-integrity rules: prices are euro cents and authoritative as returned, never summed or rounded by the model; MagentaEINS is a conditional saving, never an applied one; a payment token is an authorisation, not a completed payment.

Voice surfaces get *_spoken variants (monthly_spoken, monthly_total_spoken) so “€39.95” is read as “39 euro 95” rather than digit by digit.

Driving the demo

Three prompt templates seed a journey. Pick one from the client’s prompt menu:

Prompt What it opens
mobile_acquisition Tariff, handset, cart, checkout, consents, order
fixed_acquisition DSL and fibre, including the installation-appointment consent
ott_acquisition MagentaTV and MagentaSport, including the age confirmation

Or just type: “I need a Telekom mobile plan with about 30 GB and an iPhone.”

Expected tool sequence:

search_tariffs → get_tariff_details → search_devices → get_device_details
  → create_cart → update_cart (name, DOB, address, phone)
  → get_payment_methods (tokenise=true) → get_required_consents (grant=[...])
  → complete_order

What to point at while presenting

The presentation is ours, on every surface. Price and product reach the customer as Telekom authored them, not as a language model’s paraphrase — as a rendered widget where the client supports one, and as server-rendered display_markdown everywhere else. The model is told which, per request.

It degrades honestly. SEP-2133 requires a UI-bound tool to still be useful without the extension. respond() in server.py branches on client_supports_apps(): widget present, suppress prose; no widget, print the server’s own markdown. The failure mode this avoids is the demo telling the model “the widget already shows the price” when nothing is on screen — which silently hides pricing.

Cards only, and the server says why. get_payment_methods returns SEPA as unsupported with the reason attached: a card credential can be tokenised and delegated to an agent under the AP2 mandate model, but a SEPA mandate is an authorisation granted to the creditor and cannot travel through an agent. The token it issues is scoped to merchant, cart and a maximum amount, and expires in 15 minutes.

Consents are gated server-side, not by prompt. complete_order refuses with a list of blockers until the cart has a tariff, all four customer fields, a payment token and every mandatory consent. Try asking the model to skip them — it cannot.

The order confirmation carries a QR code generated at order time, encoding the MeinMagenta download URL with the order ID appended, to move the customer from the agent surface into our own app.

Guard rails worth demonstrating

Try this What happens
Order before granting consents Refused, with each missing consent named
Give a date of birth under 18 Refused — contract capacity check
Send a full card number Refused — the tool accepts at most four digits
Combine a device with a fixed-line tariff Refused — not device-eligible
Ask for a price the model invents The widget shows the server’s price; the tool result tells the model not to restate it

Changing the offer

Edit catalogue/tariffs.md and restart. Every block under a ## ID heading becomes one tariff; fields are key: value; prices are in euro cents. The same applies to devices and consents. The three files are also exposed as MCP resources, so the assistant can read the raw catalogue if asked.

What this demo deliberately does not do

  • No OAuth. Every session is anonymous, so there is no customer context, no eligibility check against a real account, and no upgrade window.
  • No handoff to OneShop. The order completes in-session to show the full flow; a production build would hand off a signed, single-use checkout URL at the consent step instead.
  • No ACP or UCP checkout endpoints. The tool layer is deliberately protocol-neutral; adapters would sit at the edge.
  • Prices, availability and device line-up are illustrative.
View the full README on GitHub

Install

npx cloudflared tunnel --url http://localhost:8000
New to MCP? Start with the mcpskill.org guide →