Class: Portage::Ucp::CapabilityNegotiator
- Inherits:
-
Object
- Object
- Portage::Ucp::CapabilityNegotiator
- Defined in:
- lib/portage/ucp/capability_negotiator.rb
Overview
Reconciles UCP's capability-version negotiation with MCP's own transport-level capability negotiation (§10) — they answer different questions and both apply regardless of transport:
- MCP
initializenegotiates protocol features (tools/resources/prompts). - This negotiator picks, per capability, which version both sides speak: the platform's advertised versions intersected with the versions this server (its registry + adapter) actually advertises.
Over HTTP the platform's advertised versions come from the UCP-Agent
header; over stdio there's no header, so they arrive in initialize
params instead. Either way this class only deals in the parsed version
list — transports are responsible for extracting it from their own
request shape.
§23 step 3: §12 promises a capability_negotiated event here, but
#negotiate has no call site anywhere in this gem outside its own spec
— nothing in the request path (transport initialize handling, or
otherwise) invokes it yet. Wiring the event would mean building that
call site first, which is a bigger change than threading a logger
through an existing collaborator; left undone, and cut from §12
rather than promised.
§25: the reason there's no call site is structural, not just
unbuilt. mcp 0.25.0's MCP::Server#init
(lib/mcp/server.rb:608) never reads params[:_meta] and never calls
add_instrumentation_data, unlike call_tool — so nothing at
initialize time reaches around_request/instrumentation_callback
or the _meta mechanism Mcp::Server.correlation_id_for already
relies on (§23/§24). And over HTTP,
StreamableHTTPTransport#handle_initialization
(lib/mcp/server/transports/streamable_http_transport.rb:812) builds
a Rack::Request with full headers but only pulls HTTP_ORIGIN out
of it before handing the raw JSON body string to
ServerSession#handle_json — a UCP-Agent header never crosses into
the server at all. Wiring #negotiate for real means monkeypatching
both (subclass/prepend MCP::Server#init to capture
clientInfo/_meta, and StreamableHTTPTransport#handle_initialization
to capture the UCP-Agent header), coupling this gem to mcp
internals that could silently break on a mcp gem upgrade.
Revisiting this needs either an upstream mcp gem hook at
initialize time, or a deliberate decision to accept the monkeypatch
coupling — not something to build silently as a side effect of
another task.
Instance Method Summary collapse
-
#initialize(registry: CapabilityRegistry.default) ⇒ CapabilityNegotiator
constructor
A new instance of CapabilityNegotiator.
-
#negotiate(adapter:, platform_versions: {}) ⇒ Hash<String, String>
Capability name => negotiated version.
Constructor Details
#initialize(registry: CapabilityRegistry.default) ⇒ CapabilityNegotiator
Returns a new instance of CapabilityNegotiator.
47 48 49 |
# File 'lib/portage/ucp/capability_negotiator.rb', line 47 def initialize(registry: CapabilityRegistry.default) @registry = registry end |
Instance Method Details
#negotiate(adapter:, platform_versions: {}) ⇒ Hash<String, String>
Returns capability name => negotiated version. A capability with no version in common is omitted, not raised on — it simply isn't usable for this session.
59 60 61 62 63 64 65 66 67 |
# File 'lib/portage/ucp/capability_negotiator.rb', line 59 def negotiate(adapter:, platform_versions: {}) @registry.advertised(adapter).each_with_object({}) do |capability, negotiated| business_versions = [capability.version] offered = platform_versions[capability.name] version = offered ? (offered & business_versions).first : business_versions.max negotiated[capability.name] = version if version end end |