Skip to main content
Glama
Elegant-Lift-Sweden-AB

Lift-Spares Parts Finder

Official

Lift-Spares Parts Finder (MCP server)

A public MCP server from Lift-Spares (Elegant Lift Sweden AB). Add it to the AI assistant you already use and it identifies lift (elevator) spare parts in the Lift-Spares catalog: by part number, description, make, model or part type, in any language. Ordering happens on the website; the server hands you the product page and a cart link. Parts that are in the catalog but not on the website, or not in the catalog at all, can be requested from the same chat.

Endpoint: https://mcp.lift-spares.se/mcp (streamable HTTP).

Sign in

When you add the server, your assistant opens a sign-in page: sign in with your Lift-Spares customer account (the same e-mail + one-time code as on lift-spares.se). No API keys. If you do not have an account, create one at https://lift-spares.se/account first.

Related MCP server: Ecomm MCP Server

Add it to your assistant

Assistant

How

Claude.ai / Claude desktop

Settings → Connectors → Add custom connector → URL https://mcp.lift-spares.se/mcp → sign in.

Claude Code

claude mcp add --transport http lift-spares https://mcp.lift-spares.se/mcp, then /mcp to sign in.

ChatGPT (developer mode)

Settings → Connectors → Create → MCP server URL https://mcp.lift-spares.se/mcp, authentication OAuth. Untested at the time of writing.

Cursor

Settings → MCP → Add new global MCP server: {"mcpServers": {"lift-spares": {"url": "https://mcp.lift-spares.se/mcp"}}}, then sign in from the MCP settings.

Any other MCP client that supports streamable HTTP with OAuth (dynamic client registration) works the same way.

The three tools

liftspares_search_parts(query, max_rows=10)

Search by the customer's own words in any language. Word search and meaning search run together; an exact part number comes first with at most five possible alternatives.

> search KM587123G01
items: [{product_uid: "P142409", manufacturer: "KONE", title: "KONE door roller ...", sku: "KM587123G01",
         listing: "shop", storefront_url: "https://lift-spares.se/en/products/...", cart_url: "https://lift-spares.se/cart/<variant>:1",
         found_by: "words", relevance: 0.97, matched_in: "KM587123G01 in own part number"}, ...]
count: 3, total_word_matches: 3, note: null, next_step: "..."
  • listing: "shop" — on the website; storefront_url is the product page, cart_url adds one to the cart.

  • listing: "catalog" — in the Lift-Spares database but not on the website: requestable with liftspares_request_part and its product_uid.

  • total_word_matches larger than count means the list was cut: search again with one more word.

  • No prices and no stock levels, by design: they are on the product page.

liftspares_get_part(product_uid)

One part's full identification row: all titles, all identifiers, barcode, vendor, type, tags, description, HS code, weight, country of origin, listing and links. Unknown product_uid is an error.

liftspares_request_part(description, product_uid?, manufacturer?, part_number?, quantity?, searched?)

Ask Lift-Spares for a part that is not on the website. The request is recorded for the Lift-Spares team and you get a reference (D-123). No order is placed and no reply arrives in the chat; Lift-Spares reads the requests and adds parts to the website.

Ordering

The server never orders. It gives you the product page (storefront_url) and Shopify's cart permalink (cart_url, https://lift-spares.se/cart/<variant>:1); price, availability and checkout are on lift-spares.se. Agents that speak Shopify's Universal Commerce Protocol can also use the store's own MCP endpoint, https://lift-spares.se/api/ucp/mcp (catalog, cart, checkout), which this server does not wrap.

Privacy

Every search, lookup and request is recorded together with the signed-in customer's id, the client name and version, the MCP session id and the user agent, to improve the catalog: parts people look for and Lift-Spares does not carry are what gets added next. No e-mail address, name or IP address is stored with the lookups. Traces may be kept in Lift-Spares' own Langfuse instance for the same purpose. Contact: https://lift-spares.se/pages/contact.

Self-hosting

The code is small on purpose: server.py (three tools), hybrid.py (the search: word search + meaning search + reranker, the same file the Lift-Spares chat uses), auth.py (Cloudflare Access JWT), catalog.py (Postgres), demand.py (request/lookup recording), trace.py (Langfuse, optional).

uv sync --frozen
cp deploy/service.env.example service.env   # fill in
uv run python server.py

Environment (see deploy/service.env.example):

Variable

Meaning

MCP_HOST, MCP_PORT

bind address (default 127.0.0.1:8769); MCP_HOST:MCP_PORT, mcp.lift-spares.se and 127.0.0.1:* are the accepted Host headers

DATABASE_URI

Postgres DSN of the read-only role (see Database contract)

ACCESS_TEAM_DOMAIN

Cloudflare Access team domain, e.g. lift-spares.cloudflareaccess.com (JWT issuer and key source)

ACCESS_AUD

the Access application's AUD tag (JWT audience)

ACCESS_JWKS_URL

optional override of https://<team>/cdn-cgi/access/certs (tests)

CLAIM_CUSTOMER_ID

dotted path to the customer id in the JWT claims, default custom.sub (Access copies identity-provider claims into custom); falls back to the Access sub

MCP_CUSTOMER_RPM

lookups per customer per minute; above it the next call is refused for a minute (default 60)

LITELLM_BASE_URL, LITELLM_SEARCH_KEY

the LiteLLM proxy serving the embedding and rerank models

CHATKIT_SEARCH_EMBED_MODEL, CHATKIT_SEARCH_RERANK_MODEL

model names on that proxy (empty rerank model = no rerank stage)

LANGFUSE_BASE_URL, LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_TRACING_ENVIRONMENT

optional tracing; unset keys = no tracing

Authentication is not in this code: Cloudflare Access (Managed OAuth) in front of the host signs the customer in with the Shopify customer account (generic OIDC identity provider), serves the OAuth discovery and the 401 for anonymous clients, and forwards Cf-Access-Jwt-Assertion to the origin. auth.py validates that JWT (RS256, JWKS, aud, iss) on every request and refuses everything else. A systemd unit is in deploy/.

Database contract

The server needs one login role with SELECT on the view chatkit_public.product (price-free: product_uid, manufacturer, shop_title, canonical_title, sku, primary_code, all_titles, all_identifiers, barcode, vendor, product_type, tags, description_html, hs_code, weight, country_of_origin, featured_image_url, storefront_url, shopify_status, shopify_variant_id) and EXECUTE on:

  • chatkit_public.find(words text, max_rows int) — the word search (product_uid, manufacturer, canonical_title, primary_code, sku, all_identifiers, product_type, shopify_status, storefront_url, words_matched, words_searched, matched_in, score, total_matches);

  • chatkit_public.find_similar(query_vec halfvec, max_rows int) — the meaning search, same columns;

  • demand.record(jsonb) RETURNS (id bigint, recent_count int) — inserts one lookup row and returns the caller's lookups in the last 60 s (the rate limiter). It refuses payloads carrying personal-data keys.

Statement timeouts are a role setting (ALTER ROLE ... SET statement_timeout), not code.

Tests

uv run pytest

Unit tests only: an in-test RS256 key pair stands in for Cloudflare Access, fakes stand in for Postgres and LiteLLM; tests/test_no_secrets.py guards the tree against committed keys.

License

Apache-2.0, Copyright 2026 Elegant Lift Sweden AB.

Related MCP Connectors

Related MCP Servers