silpo-py-mcp

Typed Python client for the official Silpo MCP server
(https://mcp.silpo.ua/mcp).
Built on FastMCP 3.4.7 for the Silpo AI Factory
hackathon. One library, two modes:
- Real server — Streamable HTTP transport with OAuth 2.1 + PKCE, encrypted
on-disk token storage, 40 typed methods mirroring the documented
silpo_*tools. - In-memory mock — a FastMCP server that implements the same 40 tools with realistic fixtures, so you can develop and test without a Silpo account.
Requires Python 3.12+.
Install
pip install silpo-py-mcp
# or with uv
uv add silpo-py-mcp
# local development
uv sync
Quick start (mock — no auth needed)
import asyncio
from silpo_py_mcp import SilpoClient
async def main() -> None:
async with SilpoClient.for_mock() as client:
result = await client.get_products(
"bran-1",
"DeliveryHome",
"2026-09-06T10:00:00+03:00",
"2026-09-06T11:00:00+03:00",
category="Молочні продукти",
)
for product in result.items:
print(product.title, product.price)
cart = await client.get_cart()
await client.add_or_update_cart_products(
cart.cart_id,
[
{
"productId": product.product_id,
"companyId": product.company_id,
"branchId": product.branch_id,
"quantity": 2,
}
for product in result.items
],
)
full = await client.get_cart_by_id(cart.cart_id)
print("Total:", full.totals.total_price)
asyncio.run(main())
Quick start (real server)
The first connection opens a browser for login at auth.silpo.ua
(phone + OTP or password). Tokens are encrypted and stored on disk, and the
client refreshes them automatically.
import asyncio
from silpo_py_mcp import SilpoClient
async def main() -> None:
async with SilpoClient.for_real_server() as client:
tools = await client.list_tools()
print(f"Connected. {len(tools)} tools available.")
branches = await client.call_tool("silpo_list_branches", {"limit": 1})
print("Branch:", branches["branches"][0]["address"])
asyncio.run(main())
Note on typed methods vs the real server. The typed methods and the mock mirror the live
tools/listschemas and response shapes (verified Sep 2026; re-verified Sep 7 2026; reconciled with server release-1.110.0 on Sep 10 2026; reconciled with server release-1.110.1 on Sep 11 2026; re-verified Sep 14 2026 — still 40 tools, no renames, no required-set changes; reconciled with server release-1.111.0/1.111.1 on Sep 25 2026 — still 40 tools, no renames). Context arguments such asbranchId/deliveryType/timeslotStart/timeslotEndare required where the live schema requires them — since 1.110.1 this includessilpo_get_similar_products— cart tools takeshoppingCartId, andsilpo_add_or_update_cart_productstakesproducts([{productId, companyId, branchId, quantity, addQuantity?, comment?}]— omitted/falseaddQuantityreplaces the quantity,trueadds to it).silpo_add_or_update_certificatestakescertificatesToAdd/certificatesToRemoveas[{barcode, pincode?}]objects (plain barcode strings are converted automatically). Product results carrydisplayPrice(fromPrice/toPricefilter by it, not byprice);get_productsrequires at least one ofcategory/mustHavePromotion/promotionCode/set(release-1.111.0: missing filter raisesValueErrorclient-side with the accepted-filter list;timeslotStart/timeslotEndare not validated server-side — a bogus slot silently returns the full catalog, so pass a real slot fromget_time_slots);get_time_slotssendsdeliveryTypes(singulardeliveryTypeaccepted server-side as an alias since 1.111.0; millisecond timestamps stripped since 1.111.1) and each slot carriesserviceFee(SelfPickup "Сервісний збір");find_addresssurfaceswarning/houseNumberMatchedfor unmatched house numbers (release-1.111.1 — check before relying on coordinates);get_available_delivery_typescoordinates are only validated for home-delivery types (SelfPickup/NovaPoshtaalways returned);get_product_detailscarriesdisplayPrice/image/specialPrices/externalProductIdplushasOfferAtBranch(release-1.110.0:price/displayPrice/stock/availableare the requested branch's real offer — checkhas_offer_at_branchfirst,Falsemeans no real offer at that branch);get_cart_by_idmapsserviceFeefrom the top level orcalculation(release-1.111.1); batch empty entries are skipped (meta.droppedCount→BatchProductResult.dropped_count). Coupon eligibility comes fromcanBeAppliedToOrderonsilpo_get_coupon_details— never infer it fromactive/statealone. The mock cart tools (silpo_get_shopping_cart_by_id,silpo_clear_shopping_cart, ...) accept onlyshoppingCartId— the legacycartIdalias was removed in 0.3.1 to match the live schema.call_toolalways passes arguments through verbatim for one-off calls. Responses come back JSON-like (nested FastMCPRootdataclasses are unwrapped automatically).
Smoke test against the real server
examples/real_smoke.py verifies the live contract and runs a read-only
battery of calls:
uv run examples/real_smoke.py
On the first run a browser opens for login at auth.silpo.ua; afterwards the
encrypted token in ~/.silpo_py_mcp is reused. The script checks:
- live
tools/listmatches the 40 documented tools and prints every live signature (arg names/types), - a read-only battery of
call_toolcalls built from the live schemas (branches, address, delivery types, time slots, categories tree, promotions, products, profile, favorites, loyalty, coupons, orders, promos, certificates).
Failures are reported per check without aborting — server-side schema bugs and
drift between the real server and the mock show up as ✗ lines. Exits
non-zero if the tool-name contract is violated or a battery call fails.
Known server-side quirks (re-verified live, Sep 2026 — mitigated in examples/real_smoke.py)
| Tool | Symptom | Mitigation |
|---|---|---|
silpo_get_products |
400 Bad Request on plain limit without filter (pre-1.111.0; now a clear message listing category/mustHavePromotion/promotionCode/set) |
smoke uses category or set: klatsniznyzhky; typed get_products raises ValueError before the request |
silpo_get_products |
bogus timeslotStart/timeslotEnd silently returns the full catalog (documented 1.111.0, not validated) |
always pass a real slot from get_time_slots |
silpo_get_time_slots |
-32602 for deliveryTypes: ["B2B"] |
smoke filters B2B from get_available_delivery_types |
silpo_get_my_favorites |
Cannot read properties of null (reading 'id') — a corrupted favorites entry on the server side. The typed get_favorites() raises SilpoToolExecutionError; it is not a client/model drift. |
smoke treats it as skipped; until Silpo fixes it, wrap get_favorites() in try/except SilpoToolExecutionError or use call_tool("silpo_get_my_favorites", ...) and handle the failure |
silpo_get_product_details |
slug: null chain failure |
resolved once get_products returns real slugs |
Previously reported quirks that no longer reproduce (re-verified live, Sep 2026):
silpo_get_category no longer triggers the fastmcp id rejection (it validates
cleanly), and silpo_get_my_certificates — although still intermittently returning
HTTP 500 — now responds with a normal certificates envelope (unwrapped by the
client) that validates cleanly when it does respond.
Configuration
Configuration is read from environment variables (prefix SILPO_) or a .env
file. Key settings:
| Variable | Default | Description |
|---|---|---|
SILPO_MCP_URL |
https://mcp.silpo.ua/mcp |
Server endpoint |
SILPO_OAUTH_STORAGE_DIR |
~/.silpo_py_mcp |
Encrypted token store location |
SILPO_OAUTH_ENCRYPTION_KEY |
auto-generated | Fernet key (base64) |
SILPO_OAUTH_CLIENT_NAME |
silpo-py-mcp |
Client name for OAuth registration |
SILPO_OAUTH_TOKEN_ENDPOINT_AUTH_METHOD |
none |
DCR auth method: none (public client + PKCE, default), client_secret_post, client_secret_basic |
SILPO_OAUTH_CALLBACK_TIMEOUT |
300.0 |
Seconds to wait for the browser callback |
SILPO_DEFAULT_REQUEST_TIMEOUT |
30.0 |
Per-request timeout |
SILPO_MAX_RATE_LIMIT_RETRIES |
3 |
Retries on HTTP 429 |
Programmatic overrides are supported via SilpoSettings(...) or
SilpoClient.from_fastmcp(client, mcp_url=...).
Schema-driven by design
The exact tool schemas (arguments, JSON Schema) are only known from
tools/list after authentication, per the official docs.
SilpoClient therefore exposes:
list_tools()— the live schemas from the server.call_tool(name, arguments)— pass-through calls with typed error mapping.- Typed convenience methods — wrappers over the live tool schemas
(
get_products,get_cart_by_id,add_or_update_cart_products, ...).
If Silpo renames or reshapes tools, only the affected convenience method needs
updating; call_tool keeps working.
Error handling
silpo_py_mcp.exceptions maps Silpo's documented error responses:
| Server response | Raised |
|---|---|
401 invalid_token |
SilpoAuthError |
403 |
SilpoForbiddenError |
429 (rate limit) |
SilpoRateLimitError |
-32601 method not found |
SilpoToolNotFoundError |
| Other tool failures | SilpoToolExecutionError |
| Schema mismatch / bad response | SilpoValidationError |
| Connection / protocol failures | SilpoConnectionError |
Development
uv sync # install deps
uv run pytest # run tests (all against the in-memory mock)
uv run ruff format . # format
uv run ruff check . # lint
uv run pyrefly check # type check (strict)
uv run pre-commit install # install git hooks (format/lint/type/tests)
Project layout
src/silpo_py_mcp/
├── client.py # SilpoClient — typed methods + error mapping
├── mock_server.py # SilpoMockServer — in-memory FastMCP server (40 tools)
├── auth.py # OAuth 2.1 + PKCE helper, encrypted token storage
├── config.py # pydantic-settings configuration
├── exceptions.py # typed exceptions
└── models/ # Pydantic models (product, cart, branch, category, order)
License
MIT
Release files for silpo-py-mcp 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| silpo_py_mcp-0.6.0.tar.gz | 40.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| silpo_py_mcp-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 87.4 kB
Release files / silpo_py_mcp-0.6.0.tar.gz
| Download URL | silpo_py_mcp-0.6.0.tar.gz |
|---|---|
| Size | 40.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cda9ec6e4a401604c7b5581556065e898cebb29e392af59d70576a1740fa2c17
|
|
BLAKE2b-256 checksum How to use checksums |
812f2042498d713534e80b141b0454aee6ce0aefd5a8c6741dda9cac6846ccb2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / silpo_py_mcp-0.6.0-py3-none-any.whl
| Download URL | silpo_py_mcp-0.6.0-py3-none-any.whl |
|---|---|
| Size | 46.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
179699c26368eb26fab87b180df9773275d4eb8f5e87401f2b046b77a8c8bb09
|
|
BLAKE2b-256 checksum How to use checksums |
a0c44ccba2218053ff4aac86570e11065a4c7559e620b96d051411c675d25146
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|