portage-ucp-client 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +7 -5
- data/lib/portage/ucp/client/transports/http.rb +14 -2
- data/lib/portage/ucp/client/version.rb +1 -1
- data/lib/portage/ucp/client.rb +34 -10
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1a9cbc441c1893347b4f899fe968932fb297f626285f37d848d65d2ca91e749
|
|
4
|
+
data.tar.gz: 0455b81249ce937ccbc88fb3ee19cf5590e1b0c94a187490ffb35116863bbf72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ef87e49709386c5f6f05ac388526ef9df1bedf8f7f2d5e6609a26b769d2ba3c9f56d5e4160e03eced891f502a55c795c340ffd17d09b694c8f4fe5d8ee81bb7
|
|
7
|
+
data.tar.gz: 712af022a1faea02752ba88b2023e34ec893bf567d6f8d2cc237ccf15981994fb7f85bee20124368369f4984c43699ca011b2d4af194ed308fc128a3dd46d0af
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to this project are documented here. Format loosely follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/); this project is
|
|
5
5
|
pre-1.0, so APIs may still shift between minor versions.
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.7.0] - 2026-10-22
|
|
10
|
+
|
|
11
|
+
- **Changed: requires `mcp` `~> 1.8`** (was `~> 0.24`), which serves MCP 2026-07-28 through `server/discover` and keeps `initialize` for 2025-11-25 and earlier. Minor bump because the `mcp` requirement moved from 0.x to 1.x; no API change and no changes under `lib/`. `portage-ucp` stays at `~> 0.8`: the client calls nothing newer, and the shared `mcp` `~> 1.8` requirement makes resolution pick `portage-ucp` 0.17.0 or later.
|
|
12
|
+
- Documentation only, no code change. Fixes a wrong comment on
|
|
13
|
+
`Client.ucp_section`. It said `Portage::Ucp::Manifest` still emits the old
|
|
14
|
+
flat manifest shape. It hasn't since `portage-ucp` 0.8.0, which nests
|
|
15
|
+
everything under `ucp` like live Shopify stores do. The flat fallback
|
|
16
|
+
stays, and the comment now says why: a store may still serve a manifest
|
|
17
|
+
from an older `portage-ucp`.
|
|
18
|
+
|
|
19
|
+
## [0.6.3] - 2026-09-24
|
|
20
|
+
|
|
21
|
+
- `Client.connect(url:, proxy:)` and `Client.discover(url, proxy:)` take a
|
|
22
|
+
`proxy:` option (a URL string, Hash or `URI`), passed straight to
|
|
23
|
+
Faraday's own `proxy=` on the Streamable HTTP transport. Faraday already
|
|
24
|
+
reads the env proxy vars itself; this adds an explicit override.
|
|
25
|
+
- Every store-facing request now identifies itself: `Client::USER_AGENT`
|
|
26
|
+
(`"portage-ucp-client/<ver> (+https://github.com/tomtom87/Portage)"`) is
|
|
27
|
+
sent on the manifest GET and every UCP transport call, via
|
|
28
|
+
`Client.with_user_agent(headers)`. A caller's own `User-Agent` header
|
|
29
|
+
still wins (case-insensitive). `discover(url, headers: {})` now forwards
|
|
30
|
+
`headers` to both the manifest GET and the transport it builds.
|
|
31
|
+
`Transports::Http` merges the UA into its `MCP::Client::HTTP` headers.
|
|
32
|
+
|
|
7
33
|
## [0.6.2] - 2026-09-23
|
|
8
34
|
|
|
9
35
|
- Fix: the loopback and stdio transports dropped `cart_id` from every call,
|
data/README.md
CHANGED
|
@@ -15,7 +15,7 @@ got:
|
|
|
15
15
|
| `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. |
|
|
16
16
|
| `Portage::Ucp::Client.connect(command: ...)` | stdio, spawns a subprocess (an MCP server exe). |
|
|
17
17
|
| `Portage::Ucp::Client.connect(url: ...)` | Streamable HTTP, connects to a remote MCP endpoint. |
|
|
18
|
-
| `Portage::Ucp::Client.discover(url)` | Fetches `<url>/.well-known/ucp`, parses the manifest, and connects to
|
|
18
|
+
| `Portage::Ucp::Client.discover(url)` | Fetches `<url>/.well-known/ucp`, parses the manifest, and connects to its `mcp` service — the entry point for buying from a store you've never talked to before. |
|
|
19
19
|
|
|
20
20
|
Depends only on `portage-ucp` and the `mcp` gem's client half — no adapter gem is
|
|
21
21
|
a dependency.
|
|
@@ -39,11 +39,13 @@ require "portage/ucp/client"
|
|
|
39
39
|
|
|
40
40
|
# Discover and connect to a live store you've never talked to before:
|
|
41
41
|
session = Portage::Ucp::Client.discover("https://your-shop.example")
|
|
42
|
+
meta = { agent_profile: "https://example.com/agent-profile.json" } # required over HTTP
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
results = session.search_catalog(query: "snowboard", limit: 5, meta: meta) # string-keyed Hash, shaped by the store
|
|
45
|
+
product_id = "..." # pick from results
|
|
46
|
+
checkout = session.create_checkout(line_items: [{ product_id: product_id, quantity: 1 }], meta: meta)
|
|
47
|
+
completed = session.complete_checkout(checkout_id: checkout["id"], payment_token: "spt_1a2b3c...", meta: meta)
|
|
48
|
+
order = session.get_order(order_id: "gid://shopify/Order/9001", meta: meta)
|
|
47
49
|
```
|
|
48
50
|
|
|
49
51
|
`Session` generates an `idempotency_key` for you on every mutating call unless you
|
|
@@ -25,8 +25,20 @@ module Portage
|
|
|
25
25
|
class Http
|
|
26
26
|
include UcpWireShape
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
# @param proxy [String, Hash, URI, nil] docs/plans/proxy-support.md
|
|
29
|
+
# Phase 1's "lighter touch" for this gem: Faraday already reads
|
|
30
|
+
# the env proxy vars itself (`Faraday.ignore_env_proxy` is
|
|
31
|
+
# false) and accepts a `proxy:` connection option, so this just
|
|
32
|
+
# wires that option through rather than reimplementing Faraday's
|
|
33
|
+
# own proxy handling. Accepts anything
|
|
34
|
+
# `Faraday::Connection#proxy=`/`Faraday::ProxyOptions.from`
|
|
35
|
+
# accepts — a bare URL string is the common case.
|
|
36
|
+
def initialize(url:, headers: {}, proxy: nil)
|
|
37
|
+
@client = ::MCP::Client.new(
|
|
38
|
+
transport: ::MCP::Client::HTTP.new(url: url, headers: Client.with_user_agent(headers)) do |faraday|
|
|
39
|
+
faraday.proxy = proxy if proxy
|
|
40
|
+
end
|
|
41
|
+
)
|
|
30
42
|
@client.connect
|
|
31
43
|
end
|
|
32
44
|
|
data/lib/portage/ucp/client.rb
CHANGED
|
@@ -22,6 +22,11 @@ module Portage
|
|
|
22
22
|
# Adapter directly, and place an order.
|
|
23
23
|
module Client
|
|
24
24
|
MANIFEST_PATH = "/.well-known/ucp".freeze
|
|
25
|
+
# Sent on every request this gem makes to a store (the manifest GET and
|
|
26
|
+
# each Streamable HTTP call) unless the caller's own headers name one,
|
|
27
|
+
# so a merchant reading its logs can tell a Portage agent from Ruby's
|
|
28
|
+
# or Faraday's default and find out what it is.
|
|
29
|
+
USER_AGENT = "portage-ucp-client/#{VERSION} (+https://github.com/tomtom87/Portage)".freeze
|
|
25
30
|
|
|
26
31
|
# Wraps an already-built Adapter directly — no subprocess/socket. Still
|
|
27
32
|
# runs the real merchant-side contract (authenticator, rate limiter,
|
|
@@ -35,11 +40,13 @@ module Portage
|
|
|
35
40
|
|
|
36
41
|
# Connects over stdio (a subprocess) or Streamable HTTP (a URL) — pass
|
|
37
42
|
# exactly one of `command:` or `url:`.
|
|
38
|
-
|
|
43
|
+
# @param proxy [String, Hash, nil] forwarded to Transports::Http (a
|
|
44
|
+
# `url:` connection only) — see its own doc comment.
|
|
45
|
+
def self.connect(command: nil, args: [], env: nil, url: nil, headers: {}, capabilities: nil, proxy: nil)
|
|
39
46
|
transport = if command
|
|
40
47
|
Transports::Stdio.new(command: command, args: args, env: env)
|
|
41
48
|
elsif url
|
|
42
|
-
Transports::Http.new(url: url, headers: headers)
|
|
49
|
+
Transports::Http.new(url: url, headers: headers, proxy: proxy)
|
|
43
50
|
else
|
|
44
51
|
raise ArgumentError, "connect requires either command: or url:"
|
|
45
52
|
end
|
|
@@ -51,14 +58,19 @@ module Portage
|
|
|
51
58
|
# Portage::Ucp::Manifest#services — the core-gem fix this client
|
|
52
59
|
# depends on to know where to connect).
|
|
53
60
|
# @return [Session] scoped to the manifest's advertised capabilities.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
# @param headers [Hash{String => String}] sent with the manifest GET
|
|
62
|
+
# and every call after it — e.g. a "User-Agent" naming the app
|
|
63
|
+
# built on this gem.
|
|
64
|
+
# @param proxy [String, Hash, nil] forwarded to the Streamable HTTP
|
|
65
|
+
# connection this discovers into — see Transports::Http.
|
|
66
|
+
def self.discover(url, headers: {}, proxy: nil)
|
|
67
|
+
manifest = fetch_manifest(url, headers)
|
|
68
|
+
connect(url: mcp_endpoint(manifest), headers: headers, capabilities: capability_names(manifest), proxy: proxy)
|
|
57
69
|
end
|
|
58
70
|
|
|
59
|
-
def self.fetch_manifest(url)
|
|
71
|
+
def self.fetch_manifest(url, headers = {})
|
|
60
72
|
uri = URI.parse("#{url.to_s.sub(%r{/\z}, '')}#{MANIFEST_PATH}")
|
|
61
|
-
response = Net::HTTP.get_response(uri)
|
|
73
|
+
response = Net::HTTP.get_response(uri, with_user_agent(headers))
|
|
62
74
|
raise DiscoveryError, "GET #{uri} returned #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
63
75
|
|
|
64
76
|
JSON.parse(response.body)
|
|
@@ -71,11 +83,23 @@ module Portage
|
|
|
71
83
|
end
|
|
72
84
|
private_class_method :fetch_manifest
|
|
73
85
|
|
|
86
|
+
# USER_AGENT unless `headers` already names one, in any case.
|
|
87
|
+
def self.with_user_agent(headers)
|
|
88
|
+
return headers if headers.keys.any? { |name| name.to_s.casecmp?("user-agent") }
|
|
89
|
+
|
|
90
|
+
{ "User-Agent" => USER_AGENT }.merge(headers)
|
|
91
|
+
end
|
|
92
|
+
|
|
74
93
|
# Real UCP manifests (confirmed live on Casper, Allbirds, Glossier, and
|
|
75
94
|
# 34+ other Shopify UCP rollouts as of "2026-08-25") nest everything one
|
|
76
|
-
# level deeper under a "ucp" key.
|
|
77
|
-
#
|
|
78
|
-
#
|
|
95
|
+
# level deeper under a "ucp" key. Portage::Ucp::Manifest has emitted
|
|
96
|
+
# the same nested shape since portage-ucp 0.8.0. The flat fallback
|
|
97
|
+
# stays for stores still serving a manifest from an older portage-ucp:
|
|
98
|
+
# there, services and capabilities sit at the top level and
|
|
99
|
+
# capabilities is an array of {name:, version:}. This gem needing
|
|
100
|
+
# portage-ucp 0.8 says nothing about what a store runs, so dropping
|
|
101
|
+
# the fallback would break discovery against those stores — see
|
|
102
|
+
# docs/well-known-ucp.md.
|
|
79
103
|
def self.ucp_section(manifest)
|
|
80
104
|
manifest["ucp"] || manifest
|
|
81
105
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: portage-ucp-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tom Whitbread
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -30,14 +30,14 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
33
|
+
version: '1.8'
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
40
|
+
version: '1.8'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: portage-ucp
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|