ruby-mcp-client 2.1.0 → 3.0.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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/OAUTH.md +555 -0
  3. data/README.md +825 -48
  4. data/lib/mcp_client/audio_content.rb +1 -1
  5. data/lib/mcp_client/auth/browser_oauth.rb +131 -21
  6. data/lib/mcp_client/auth/oauth_provider/challenge_handling.rb +532 -0
  7. data/lib/mcp_client/auth/oauth_provider/client_authentication.rb +121 -0
  8. data/lib/mcp_client/auth/oauth_provider/pending_requests.rb +51 -0
  9. data/lib/mcp_client/auth/oauth_provider/registration_store.rb +486 -0
  10. data/lib/mcp_client/auth/oauth_provider/response_validation.rb +441 -0
  11. data/lib/mcp_client/auth/oauth_provider/scope_selection.rb +134 -0
  12. data/lib/mcp_client/auth/oauth_provider/token_store.rb +419 -0
  13. data/lib/mcp_client/auth/oauth_provider.rb +1354 -386
  14. data/lib/mcp_client/auth/peer_text.rb +174 -0
  15. data/lib/mcp_client/auth.rb +298 -32
  16. data/lib/mcp_client/cached_result.rb +145 -0
  17. data/lib/mcp_client/called_tool_definition.rb +138 -0
  18. data/lib/mcp_client/client/cache_slices.rb +195 -0
  19. data/lib/mcp_client/client/list_aggregation.rb +243 -0
  20. data/lib/mcp_client/client/notification_routing.rb +155 -0
  21. data/lib/mcp_client/client/sampling_validation.rb +200 -0
  22. data/lib/mcp_client/client/task_api.rb +531 -0
  23. data/lib/mcp_client/client/task_lifetimes.rb +269 -0
  24. data/lib/mcp_client/client/task_registry.rb +254 -0
  25. data/lib/mcp_client/client/task_shape.rb +102 -0
  26. data/lib/mcp_client/client/task_support.rb +1166 -0
  27. data/lib/mcp_client/client/task_updates.rb +457 -0
  28. data/lib/mcp_client/client/task_wait_boundaries.rb +198 -0
  29. data/lib/mcp_client/client/task_workers.rb +63 -0
  30. data/lib/mcp_client/client.rb +796 -518
  31. data/lib/mcp_client/deep_copy.rb +49 -0
  32. data/lib/mcp_client/deprecation_notices.rb +94 -0
  33. data/lib/mcp_client/deprecations.rb +419 -0
  34. data/lib/mcp_client/errors.rb +474 -7
  35. data/lib/mcp_client/header_params.rb +320 -0
  36. data/lib/mcp_client/http_transport_base/bounded_inflate.rb +41 -0
  37. data/lib/mcp_client/http_transport_base/cache_support.rb +694 -0
  38. data/lib/mcp_client/http_transport_base/era_detection.rb +134 -0
  39. data/lib/mcp_client/http_transport_base/listen_stream.rb +763 -0
  40. data/lib/mcp_client/http_transport_base/param_headers.rb +35 -0
  41. data/lib/mcp_client/http_transport_base/request_recovery.rb +156 -0
  42. data/lib/mcp_client/http_transport_base/session_recovery.rb +113 -0
  43. data/lib/mcp_client/http_transport_base/sse_event_scanner.rb +145 -0
  44. data/lib/mcp_client/http_transport_base/stream_capture.rb +160 -0
  45. data/lib/mcp_client/http_transport_base/stream_recovery.rb +318 -0
  46. data/lib/mcp_client/http_transport_base/tool_listing.rb +277 -0
  47. data/lib/mcp_client/http_transport_base.rb +666 -120
  48. data/lib/mcp_client/input_round_trips.rb +128 -0
  49. data/lib/mcp_client/json_rpc_common/envelopes.rb +32 -0
  50. data/lib/mcp_client/json_rpc_common/error_bodies.rb +105 -0
  51. data/lib/mcp_client/json_rpc_common/input_waits.rb +167 -0
  52. data/lib/mcp_client/json_rpc_common.rb +900 -13
  53. data/lib/mcp_client/oauth_client.rb +14 -5
  54. data/lib/mcp_client/prompt.rb +4 -0
  55. data/lib/mcp_client/request_authorization.rb +128 -0
  56. data/lib/mcp_client/request_meta_scope.rb +77 -0
  57. data/lib/mcp_client/request_metadata.rb +287 -0
  58. data/lib/mcp_client/resource.rb +4 -0
  59. data/lib/mcp_client/resource_content.rb +20 -0
  60. data/lib/mcp_client/resource_template.rb +4 -0
  61. data/lib/mcp_client/result_caching.rb +999 -0
  62. data/lib/mcp_client/result_completeness.rb +34 -0
  63. data/lib/mcp_client/root.rb +6 -0
  64. data/lib/mcp_client/round_trip_marker.rb +28 -0
  65. data/lib/mcp_client/schema_validator/annotations.rb +82 -0
  66. data/lib/mcp_client/schema_validator/composition.rb +86 -0
  67. data/lib/mcp_client/schema_validator/dialects.rb +66 -0
  68. data/lib/mcp_client/schema_validator/ecma_patterns.rb +567 -0
  69. data/lib/mcp_client/schema_validator/evaluation.rb +517 -0
  70. data/lib/mcp_client/schema_validator/input_requirements.rb +84 -0
  71. data/lib/mcp_client/schema_validator/instances.rb +449 -0
  72. data/lib/mcp_client/schema_validator/keyword_scan.rb +121 -0
  73. data/lib/mcp_client/schema_validator/normalization.rb +104 -0
  74. data/lib/mcp_client/schema_validator/references.rb +610 -0
  75. data/lib/mcp_client/schema_validator/scalars.rb +126 -0
  76. data/lib/mcp_client/schema_validator/shapes.rb +319 -0
  77. data/lib/mcp_client/schema_validator/uri_references.rb +153 -0
  78. data/lib/mcp_client/schema_validator.rb +882 -208
  79. data/lib/mcp_client/server_base.rb +233 -5
  80. data/lib/mcp_client/server_factory.rb +9 -3
  81. data/lib/mcp_client/server_http/json_rpc_transport.rb +219 -4
  82. data/lib/mcp_client/server_http.rb +307 -90
  83. data/lib/mcp_client/server_sse/json_rpc_transport.rb +113 -25
  84. data/lib/mcp_client/server_sse/sse_parser.rb +39 -6
  85. data/lib/mcp_client/server_sse.rb +227 -62
  86. data/lib/mcp_client/server_stdio/child_session.rb +98 -0
  87. data/lib/mcp_client/server_stdio/json_rpc_transport.rb +1003 -28
  88. data/lib/mcp_client/server_stdio.rb +772 -183
  89. data/lib/mcp_client/server_streamable_http/json_rpc_transport.rb +189 -25
  90. data/lib/mcp_client/server_streamable_http.rb +302 -115
  91. data/lib/mcp_client/session_pin.rb +119 -0
  92. data/lib/mcp_client/subscription/notification_dispatcher.rb +354 -0
  93. data/lib/mcp_client/subscription.rb +852 -0
  94. data/lib/mcp_client/subscription_support.rb +715 -0
  95. data/lib/mcp_client/task.rb +286 -14
  96. data/lib/mcp_client/tool.rb +31 -3
  97. data/lib/mcp_client/version.rb +21 -6
  98. data/lib/mcp_client.rb +108 -19
  99. metadata +68 -2
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCPClient
4
+ # The guard an operation runs before it projects a payload out of a result.
5
+ #
6
+ # An unfinished answer (MCP 2026-07-28 InputRequiredResult) that reaches
7
+ # such an operation would lose the server's inputRequests and its opaque
8
+ # requestState, and would be presented as an empty successful answer. The
9
+ # resolver that wraps every request drives the round trips a modern server
10
+ # asks for, so what reaches here is either finished or something no
11
+ # resolver claims; this is the backstop for the latter.
12
+ module ResultCompleteness
13
+ private
14
+
15
+ # @param result [Object] the JSON-RPC result
16
+ # @param method [String] the request method, for the message
17
+ # @return [Object] the result, when it is complete
18
+ # @raise [MCPClient::Errors::InputRequiredError] when it is unfinished --
19
+ # the whole result rides on the error's `data`, so a host can drive the
20
+ # round trip itself
21
+ # @raise [MCPClient::Errors::InvalidResultError] for any other
22
+ # discriminator this client cannot carry through
23
+ def require_complete_result!(result, method)
24
+ type = MCPClient::JsonRpcCommon.result_type(result)
25
+ return result if type == 'complete'
26
+
27
+ message = "#{method} answered with resultType #{type.to_s[0, 64].inspect}, which this " \
28
+ 'client cannot carry through'
29
+ raise MCPClient::Errors::InputRequiredError.new(message, data: result) if type == 'input_required'
30
+
31
+ raise MCPClient::Errors::InvalidResultError.new("Invalid result: #{message}", data: result)
32
+ end
33
+ end
34
+ end
@@ -5,6 +5,12 @@ require 'uri'
5
5
  module MCPClient
6
6
  # Represents an MCP Root - a URI that defines a boundary where servers can operate
7
7
  # Roots are declared by clients to inform servers about relevant resources and their locations
8
+ #
9
+ # @deprecated Roots is deprecated since MCP 2026-07-28 (SEP-2577); earliest
10
+ # removal is the first revision released on or after 2027-07-28. The class
11
+ # keeps working for as long as the feature does, and building one raises no
12
+ # notice — using the list does. Pass directories or files through tool
13
+ # parameters, resource URIs or server configuration instead.
8
14
  class Root
9
15
  attr_reader :uri, :name, :meta
10
16
 
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCPClient
4
+ # Whether the request a thread last resolved went through a multi
5
+ # round-trip retry (MCP 2026-07-28 InputRequiredResult). A result that
6
+ # depended on input responses MUST NOT be cached, and the marker is
7
+ # thread-local because a transport serves concurrent requests: another
8
+ # request completing meanwhile says nothing about this one.
9
+ module RoundTripMarker
10
+ private
11
+
12
+ # @return [Boolean]
13
+ def last_result_from_round_trip?
14
+ Thread.current[round_trip_marker_key] == true
15
+ end
16
+
17
+ # @param flag [Boolean]
18
+ # @return [void]
19
+ def mark_round_trip_result(flag)
20
+ Thread.current[round_trip_marker_key] = flag
21
+ end
22
+
23
+ # @return [Symbol] the thread-local key of this transport's round-trip marker
24
+ def round_trip_marker_key
25
+ :"mcp_client_round_trip_#{object_id}"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCPClient
4
+ module SchemaValidator
5
+ # What the schemas applied to one instance value evaluated of it: the
6
+ # annotation results `unevaluatedProperties` and `unevaluatedItems` read
7
+ # (JSON Schema 2020-12 Core Sections 11.2 and 11.3). One is kept per
8
+ # application of a schema to an object or an array, filled in by the
9
+ # keywords the node evaluates itself (`properties`, `patternProperties`,
10
+ # `additionalProperties`, `prefixItems`, `items`, `contains` and the two
11
+ # keywords themselves) and merged from every in-place applicator whose
12
+ # subschema passed — a failed subschema annotates nothing, and a cousin
13
+ # (a sibling branch of the same composition) never sees it.
14
+ class Evaluated
15
+ def initialize
16
+ @all = false
17
+ @names = {}
18
+ @prefix = 0
19
+ @indices = {}
20
+ end
21
+
22
+ # @return [Boolean] whether every member or item was evaluated
23
+ def all?
24
+ @all
25
+ end
26
+
27
+ # Every member or item is evaluated (an `items` schema, an
28
+ # `additionalProperties` schema, or the unevaluated keyword itself).
29
+ # @return [void]
30
+ def all!
31
+ @all = true
32
+ end
33
+
34
+ # @param name [Object] a property name (either key form)
35
+ # @return [void]
36
+ def name!(name)
37
+ @names[name.to_s] = true unless @all
38
+ end
39
+
40
+ # @param count [Integer] how many leading items a tuple evaluated
41
+ # @return [void]
42
+ def prefix!(count)
43
+ @prefix = count if count > @prefix
44
+ end
45
+
46
+ # @param index [Integer] an item `contains` matched
47
+ # @return [void]
48
+ def index!(index)
49
+ @indices[index] = true unless @all
50
+ end
51
+
52
+ # @param name [Object] a property name (either key form)
53
+ # @return [Boolean] whether the member was evaluated
54
+ def property?(name)
55
+ @all || @names.key?(name.to_s)
56
+ end
57
+
58
+ # @param index [Integer]
59
+ # @return [Boolean] whether the item was evaluated
60
+ def item?(index)
61
+ @all || index < @prefix || @indices.key?(index)
62
+ end
63
+
64
+ # Take over what a passed subschema evaluated of the same value.
65
+ # @param other [Evaluated, nil]
66
+ # @return [Evaluated] self
67
+ def merge!(other)
68
+ return self if other.nil?
69
+ return all! || self if other.all?
70
+
71
+ @names.merge!(other.names)
72
+ @prefix = other.prefix if other.prefix > @prefix
73
+ @indices.merge!(other.indices)
74
+ self
75
+ end
76
+
77
+ protected
78
+
79
+ attr_reader :names, :prefix, :indices
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCPClient
4
+ module SchemaValidator
5
+ # How complete a verdict on one value is. Extended into SchemaValidator,
6
+ # so the methods are its own; {Evaluation} applies the composition
7
+ # keywords themselves and reads the answers here.
8
+ #
9
+ # A composition branch is evaluated speculatively (its errors are a
10
+ # verdict, not output). anyOf and allOf are monotonic, so a branch that
11
+ # passes as far as the validator can evaluate it is accepted; not, oneOf
12
+ # and if are not, so a branch that still holds an unevaluated assertion
13
+ # applying to the instance is :undecided and never a match, while one
14
+ # decided by its evaluated keywords (or carrying only annotations) is a
15
+ # full verdict.
16
+ #
17
+ # Every standard assertion whose verdict this validator can reach is
18
+ # evaluated ({Instances#validate_object},
19
+ # {Instances#validate_array}, {Scalars#validate_number},
20
+ # and the unevaluated keywords from the annotations {Evaluation}
21
+ # collects), so what is left here is only what genuinely cannot be
22
+ # decided: a dynamic reference whose target the dynamic scope could
23
+ # re-bind, and `format` where it asserts.
24
+ module Composition
25
+ # Whether a schema object carries an assertion the validator does not
26
+ # evaluate (in the dialect in force) that applies to this instance, so
27
+ # its verdict is only partial. Annotations (`format` in 2019-09 and
28
+ # 2020-12, `contentSchema`) decide nothing and leave the verdict whole;
29
+ # draft-07 `format` asserts (Validation Section 7.2), and the validator
30
+ # does not evaluate formats, so a string branch carrying one is
31
+ # undecided there. Dynamic references are evaluated, against the
32
+ # dynamic scope, and leave no verdict partial.
33
+ # @param schema [Hash] the schema object
34
+ # @param dialect [String, nil] the dialect in force at it
35
+ # @param data [Object] the instance
36
+ # @param _ctx [Context] the validation context
37
+ # @return [Boolean]
38
+ def partial_keywords?(schema, dialect, data, _ctx)
39
+ dialect == DRAFT_07 && data.is_a?(String) && schema.key?('format')
40
+ end
41
+
42
+ # The number of matching items `contains` requires: its companion
43
+ # where the dialect defines one and gives it a number, else the
44
+ # default of 1 (JSON Schema 2020-12 Validation Section 6.4.4).
45
+ # @return [Numeric]
46
+ def contains_min(schema, dialect)
47
+ min = schema['minContains'] if keyword_known?('minContains', dialect)
48
+ min.is_a?(Numeric) ? min : 1
49
+ end
50
+
51
+ # @return [Numeric, nil] the number of matching items `contains`
52
+ # allows, when the dialect defines the companion and it is a number
53
+ def contains_max(schema, dialect)
54
+ max = schema['maxContains'] if keyword_known?('maxContains', dialect)
55
+ max if max.is_a?(Numeric)
56
+ end
57
+
58
+ # @param data [Hash] the instance
59
+ # @param name [Object] the property name a schema keyword names
60
+ # @return [Boolean] whether the instance carries the property in either
61
+ # key form
62
+ def property_present?(data, name)
63
+ name = name.to_s
64
+ data.key?(name) || data.key?(name.to_sym)
65
+ end
66
+
67
+ # Match a server-supplied pattern against a property name. Both come
68
+ # from the peer, so — exactly like {.validate_pattern} — the match runs
69
+ # under the validation-wide deadline: a backtracking expression here
70
+ # must not be able to hold the calling thread.
71
+ # @param deadline [Float, nil] monotonic deadline for the whole validation
72
+ # @return [Boolean]
73
+ # @raise [Aborted] when the budget is exhausted
74
+ def pattern_matches?(pattern, name, deadline = nil)
75
+ remaining = pattern_budget_remaining(deadline)
76
+ raise Aborted, "validation time budget exhausted before pattern #{clip(pattern.inspect)}" if remaining.zero?
77
+
78
+ ecma_regexp(pattern, remaining, deadline).match?(name)
79
+ rescue Regexp::TimeoutError
80
+ raise Aborted, "pattern #{clip(pattern.inspect)} exceeded the #{PATTERN_MATCH_TIMEOUT}s matching budget"
81
+ rescue RegexpError, TypeError
82
+ false
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCPClient
4
+ module SchemaValidator
5
+ # Dialect selection: the `$schema` a document (or an embedded resource
6
+ # root) declares, its canonical supported form, and the problems an
7
+ # unusable declaration reports. Extended into SchemaValidator, so the
8
+ # methods are its own.
9
+ module Dialects
10
+ # The dialect a schema declares, or the default.
11
+ # @param schema [Object] the schema
12
+ # @return [String] the `$schema` value (without a trailing `#`), or DEFAULT_DIALECT
13
+ def dialect(schema)
14
+ return DEFAULT_DIALECT unless schema.is_a?(Hash)
15
+ return DEFAULT_DIALECT unless schema.key?('$schema') || schema.key?(:$schema)
16
+
17
+ declared = schema.key?('$schema') ? schema['$schema'] : schema[:$schema]
18
+ # A declaration that is present but unusable is not the default.
19
+ return nil unless declared.is_a?(String) && !declared.empty?
20
+
21
+ declared.sub(/#\z/, '')
22
+ end
23
+
24
+ # The supported dialect a declared URI stands for (scheme-insensitive),
25
+ # or nil.
26
+ # @param uri [String]
27
+ # @return [String, nil]
28
+ def canonical_dialect(uri)
29
+ canonical = uri.sub(/#\z/, '').sub(%r{\Ahttps?://}, '')
30
+ SUPPORTED_DIALECTS.find { |d| d.sub(%r{\Ahttps?://}, '') == canonical }
31
+ end
32
+
33
+ # @param uri [String]
34
+ # @return [Boolean]
35
+ def supported_dialect?(uri)
36
+ !canonical_dialect(uri).nil?
37
+ end
38
+
39
+ # The dialect in force at a schema object: an embedded resource root (a
40
+ # subschema whose `$id` is a URI) may declare its own `$schema`, which
41
+ # is that resource's dialect (JSON Schema 2020-12 Core Section 8.1.1);
42
+ # anywhere else the inherited dialect applies (a `$schema` that is not
43
+ # at a resource root is ignored).
44
+ # @param schema [Hash]
45
+ # @param inherited [String, nil] the enclosing resource's dialect
46
+ # @return [String, nil] the canonical dialect, or nil when the embedded
47
+ # declaration is malformed or unsupported
48
+ def embedded_dialect(schema, inherited)
49
+ return inherited unless resource_root?(schema, inherited) && schema.key?('$schema')
50
+
51
+ declared = dialect(schema)
52
+ declared && canonical_dialect(declared)
53
+ end
54
+
55
+ # @return [String, nil] why an embedded resource's `$schema` is unusable
56
+ def embedded_dialect_problem(schema)
57
+ declared = dialect(schema)
58
+ return 'embedded resource $schema must be a non-empty string naming the dialect' if declared.nil?
59
+ return nil if supported_dialect?(declared)
60
+
61
+ "embedded resource dialect #{clip(declared.inspect)} is not supported " \
62
+ "(supported: #{SUPPORTED_DIALECTS.join(', ')})"
63
+ end
64
+ end
65
+ end
66
+ end