portage-ucp

Protocol-only core gem: expose a commerce backend to AI shopping agents over MCP (Model Context Protocol) and UCP (Universal Commerce Protocol) at once. Zero commerce-backend dependencies — works with any backend that implements Adapter, Shopify or otherwise. Adapter gems (portage-ucp-shopify, portage-ucp-wix, ...) are consumers of this gem, not dependencies of it.

See the root README for the full walkthrough (an agent discovering a manifest and buying a snowboard end to end), security model, and adapter comparison table. This README covers just what lives in this gem.

What it ships

Class Role
Portage::Ucp::Adapter The contract your backend implements — override only the catalog/cart/checkout/order/identity methods you support; the rest stay unadvertised.
Portage::Ucp::CapabilityRegistry Figures out which capabilities an Adapter actually backs.
Portage::Ucp::Dispatcher Routes a capability+action call to the right Adapter method.
Portage::Ucp::Mcp::Server Wraps an Adapter as an MCP server — one MCP::Tool per advertised action, stdio or Streamable HTTP.
Portage::Ucp::Manifest Builds the signed /.well-known/ucp discovery document.
Portage::Ucp::Rack::ManifestEndpoint Serves that manifest over Rack.
Portage::Ucp::Rack::WebhookEndpoint HMAC-verified inbound order-lifecycle webhooks.
Portage::Ucp::SchemaValidator Validates data against UCP's own vendored JSON Schemas/OpenRPC docs, offline.
Portage::Ucp::Resolver / exe/portage-ucp-check Probes any store's homepage/.well-known/ucp and recommends the matching adapter gem.

Security defaults are all locked down, not permissive-by-omission — UnconfiguredAuthenticator rejects every mutating call until you configure a real one, PaymentTokenGuard rejects raw card numbers before they reach your Adapter, and manifest signing is opt-in. Full detail in the root README's Security hooks section.

Installation

# Gemfile
gem "portage-ucp"
bundle install

Usage

require "portage/ucp"

class MyAdapter < Portage::Ucp::Adapter
  def search_catalog(query:, limit:) = ...
  def get_product(product_id:) = ...
  def create_cart(line_items:, idempotency_key:) = ...
  # override only the capabilities you support
end

Portage::Ucp.configure do |config|
  config.authenticator = MyAuthenticator.new
  config.rate_limiter = MyRateLimiter.new
  config.business = { name: "Your Store", url: "https://your-shop.example" }
end

server = Portage::Ucp::Mcp::Server.build(adapter: MyAdapter.new)
server.start

See the root README's Usage and the detailed walkthrough for the full agent-side conversation, manifest/webhook Rack mounting, and a real adapter to model your own against.

Checking any store

bundle exec portage-ucp-check your-shop.example

Tries /.well-known/ucp first; falls back to platform detection and names the matching portage-ucp-<adapter> gem, live-probing it if credentials are already in env. See the root README's Checking any store section for sample output.

Development

bundle exec rspec
bundle exec rubocop

See the design log for the design rationale and decision history behind this project.

License

MIT — Copyright (c) 2026 Tom Whitbread.