portage-ucp-client

Client-side SDK for portage-ucp — the other direction from every adapter gem. Every adapter gem lets a Ruby program expose a commerce backend to agents (server side). This gem lets a Ruby program act as the shopper's agent: connect to somebody else's /.well-known/ucp manifest, or drive your own Adapter directly, and place an order as the client.

Three transports behind one Session interface — callers never know which they got:

Transport Use case
Portage::Ucp::Client.for_adapter(adapter) Loopback over an in-process Adapter — no subprocess, no socket. Still runs the real authenticator/rate-limiter/Dispatcher stack, just without the wire hop. For driving your own store's Adapter directly.
Portage::Ucp::Client.connect(command: ...) stdio, spawns a subprocess (an MCP server exe).
Portage::Ucp::Client.connect(url: ...) Streamable HTTP, connects to a remote MCP endpoint.
Portage::Ucp::Client.discover(url) Fetches <url>/.well-known/ucp, parses the manifest, and connects to whatever transport it advertises — the entry point for buying from a store you've never talked to before.

Depends only on portage-ucp and the mcp gem's client half — no adapter gem is a dependency.

Installation

# Gemfile
gem "portage-ucp-client"
bundle install

Usage

require "portage/ucp"
require "portage/ucp/client"

# Discover and connect to a live store you've never talked to before:
session = Portage::Ucp::Client.discover("https://your-shop.example")

products = session.search_catalog(query: "snowboard", limit: 5)
checkout = session.create_checkout(line_items: [{ product_id: products.first.id, quantity: 1 }])
completed = session.complete_checkout(checkout_id: checkout["id"], payment_token: "spt_1a2b3c...")
order = session.get_order(order_id: "gid://shopify/Order/9001")

Session generates an idempotency_key for you on every mutating call unless you pass your own — retrying with the same key replays the cached result instead of double-charging.

See the detailed walkthrough for the full runnable example (via the loopback transport) including requires_escalation handling and RawPanRejectedError, and portage-cli for a ready-made portage buy <url> command built on top of this gem.

Development

bundle exec rspec
bundle exec rubocop

License

MIT — Copyright (c) 2026 Tom Whitbread.