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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89837fec9676ad52dd925e44ea86b12df99ae2f01ea56eccd4b877bbde7dfb7a
4
- data.tar.gz: 34e0f987ba86fb975eaed185950cc6c2690c3f177ae8eacbbdb718560be4fb5e
3
+ metadata.gz: a1a9cbc441c1893347b4f899fe968932fb297f626285f37d848d65d2ca91e749
4
+ data.tar.gz: 0455b81249ce937ccbc88fb3ee19cf5590e1b0c94a187490ffb35116863bbf72
5
5
  SHA512:
6
- metadata.gz: 99581702b0ff34ed5a8fd680ffac05a05576823a6381bf312b63436107fb55977be865f7649be32827b3503b28465e3a862df9730058fe7ba4033179acd28505
7
- data.tar.gz: db7d14a2a8149ca8b526ab4fc0ee6f6f807cad84b83eb57229879659e75e5d13207f96faada575354da1981fb78bc2a1de72edc98c38405a81a5efc3ddcfa7e1
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 whatever transport it advertises — the entry point for buying from a store you've never talked to before. |
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
- products = session.search_catalog(query: "snowboard", limit: 5)
44
- checkout = session.create_checkout(line_items: [{ product_id: products.first.id, quantity: 1 }])
45
- completed = session.complete_checkout(checkout_id: checkout["id"], payment_token: "spt_1a2b3c...")
46
- order = session.get_order(order_id: "gid://shopify/Order/9001")
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
- def initialize(url:, headers: {})
29
- @client = ::MCP::Client.new(transport: ::MCP::Client::HTTP.new(url: url, headers: headers))
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
 
@@ -1,7 +1,7 @@
1
1
  module Portage
2
2
  module Ucp
3
3
  module Client
4
- VERSION = "0.6.2".freeze
4
+ VERSION = "0.7.0".freeze
5
5
  end
6
6
  end
7
7
  end
@@ -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
- def self.connect(command: nil, args: [], env: nil, url: nil, headers: {}, capabilities: nil)
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
- def self.discover(url)
55
- manifest = fetch_manifest(url)
56
- connect(url: mcp_endpoint(manifest), capabilities: capability_names(manifest))
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. This gem's own server side
77
- # (Portage::Ucp::Manifest) still emits the old flat shape, so both are
78
- # supported rather than picking one — see docs/well-known-ucp.md.
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.6.2
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-09-23 00:00:00.000000000 Z
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: '0.24'
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: '0.24'
40
+ version: '1.8'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: portage-ucp
43
43
  requirement: !ruby/object:Gem::Requirement