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,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCPClient
4
+ module Auth
5
+ # Every string an authorization server, a resource server or a browser
6
+ # callback supplies passes through here before it reaches a log line or an
7
+ # exception message.
8
+ #
9
+ # Those messages are not private: {MCPClient::Auth::BrowserOAuth} renders
10
+ # the message of a failed flow on the page it serves to the browser, and
11
+ # logs are read, forwarded and pasted. Peer-supplied bytes quoted verbatim
12
+ # carry CR/LF into a log record (splitting it into attacker-chosen lines),
13
+ # terminal escape sequences into a console, and unbounded response bodies
14
+ # into both. And the message of a JSON::ParserError quotes the token it
15
+ # choked on — "expected object key, got 'SECRET' at line 1 column 2" —
16
+ # which puts a fragment of the very body that failed to parse into the
17
+ # text.
18
+ #
19
+ # So: printable, bounded text for peer strings, and position without
20
+ # content for a parse failure. The OAuth classes define these themselves
21
+ # rather than reaching for {MCPClient::JsonRpcCommon}, which they do not
22
+ # include: a rescue path that calls a helper its object does not have
23
+ # raises NoMethodError out of the very request the rescue existed to keep
24
+ # working.
25
+ #
26
+ # Every helper here is TOTAL: no input can raise out of it. Nothing about
27
+ # a peer's bytes guarantees they are valid UTF-8 — a response body arrives
28
+ # as whatever the socket carried, a callback parameter as whatever
29
+ # `CGI.unescape` made of `%FF`, and a `JSON::ParserError` message quotes
30
+ # the undecodable bytes it choked on — and `String#gsub`, `String#strip`
31
+ # and `Regexp#match` all raise `ArgumentError: invalid byte sequence in
32
+ # UTF-8` on them. A sanitizer that raises on the input it exists to
33
+ # sanitize is worse than none: it turns a peer's `400` body, or an
34
+ # `error_description=%FF`, into an exception out of the rescue path that
35
+ # was meant to report it. Undecodable bytes are therefore replaced before
36
+ # anything else looks at them.
37
+ module PeerText
38
+ # How much peer-supplied text a message may carry.
39
+ PEER_TEXT_LIMIT = 200
40
+
41
+ # What an undecodable byte becomes. ASCII, so the result is safe to
42
+ # write to a log device of any encoding.
43
+ UNDECODABLE_BYTE = '?'
44
+
45
+ # What a helper reports when even the replacement could not be made
46
+ # (an object whose #to_s raises, a string no encoding handler accepts).
47
+ # A helper here never raises, so there is always something to say.
48
+ UNREADABLE_TEXT = '(unreadable)'
49
+
50
+ # Peer bytes as valid UTF-8, and nothing else changed: no control
51
+ # characters removed, no truncation. This is the form peer text has to
52
+ # be in BEFORE it is parsed rather than printed — a WWW-Authenticate
53
+ # header matched for its Bearer segment, an OAuth error description
54
+ # matched for a redirect_uri mismatch, a callback parameter that is
55
+ # about to be compared, logged or rendered. `String#gsub`,
56
+ # `String#match`, `Regexp#match?`, `String#split` and `String#strip` all
57
+ # raise `ArgumentError` on bytes that are not valid UTF-8, so a peer
58
+ # that sends `%FF` turns the code that reads its error into the error.
59
+ # Making the bytes decodable is not enough on its own — the printable,
60
+ # bounded form is still what reaches a message — but it is what has to
61
+ # happen first, and it must happen at the point the bytes stop being
62
+ # bytes and start being text.
63
+ #
64
+ # A module function, not only a private instance method, because the
65
+ # transports read the same WWW-Authenticate header and do not (and must
66
+ # not) take on the rest of this module: {MCPClient::JsonRpcCommon}
67
+ # already defines helpers of the same names.
68
+ # @param text [Object] peer-supplied bytes
69
+ # @return [String] the same text as valid UTF-8
70
+ def self.decodable(text)
71
+ return UNREADABLE_TEXT unless text.is_a?(String)
72
+
73
+ utf8 = text.encoding == Encoding::UTF_8 ? text : text.dup.force_encoding(Encoding::UTF_8)
74
+ utf8.valid_encoding? ? utf8 : utf8.scrub(UNDECODABLE_BYTE)
75
+ rescue StandardError
76
+ UNREADABLE_TEXT
77
+ end
78
+
79
+ # Whether a peer's bytes are already text: a String that reads as valid
80
+ # UTF-8 as it stands. What {.decodable} returns for anything else is a
81
+ # rewriting of what the peer sent — fine for a message, wrong for a
82
+ # value that is about to be used as a URL.
83
+ # @param text [Object] peer-supplied bytes
84
+ # @return [Boolean]
85
+ def self.decodable?(text)
86
+ return false unless text.is_a?(String)
87
+
88
+ (text.encoding == Encoding::UTF_8 ? text : text.dup.force_encoding(Encoding::UTF_8)).valid_encoding?
89
+ rescue StandardError
90
+ false
91
+ end
92
+
93
+ private
94
+
95
+ # @param value [Object] peer-supplied text
96
+ # @return [String, nil] printable, bounded text; nil unless a String was given
97
+ def safe_error_text(value)
98
+ return nil unless value.is_a?(String)
99
+
100
+ printable_peer_text(value)
101
+ end
102
+
103
+ # The same, for a value that is not necessarily a String (a response
104
+ # body, which Faraday may hand over as nil).
105
+ # @param value [Object] a peer-supplied response body
106
+ # @return [String] printable, bounded text, never nil
107
+ def safe_body_text(value)
108
+ return '' if value.nil?
109
+
110
+ printable_peer_text(value.is_a?(String) ? value : value.to_s)
111
+ rescue StandardError
112
+ UNREADABLE_TEXT
113
+ end
114
+
115
+ # Peer bytes as text that can be logged, matched, sliced and rendered:
116
+ # valid UTF-8 (undecodable bytes replaced), free of control characters,
117
+ # and bounded.
118
+ # @param text [String] peer-supplied bytes
119
+ # @return [String]
120
+ def printable_peer_text(text)
121
+ decodable_text(text).gsub(/[[:cntrl:]]/, ' ')[0, PEER_TEXT_LIMIT].to_s
122
+ rescue StandardError
123
+ UNREADABLE_TEXT
124
+ end
125
+
126
+ # Peer text a Regexp, #split or #strip can be run over: valid UTF-8 and
127
+ # otherwise unchanged, so a parser reads the value the peer sent rather
128
+ # than a truncated, control-stripped rendering of it.
129
+ # @param value [Object] peer-supplied text
130
+ # @return [String, nil] valid UTF-8; nil unless a String was given
131
+ def matchable_peer_text(value)
132
+ return nil unless value.is_a?(String)
133
+
134
+ PeerText.decodable(value)
135
+ end
136
+
137
+ # The same bytes as valid UTF-8. A string in another encoding (a binary
138
+ # response body, a UTF-16 message) is read as UTF-8 and scrubbed rather
139
+ # than transcoded: the point is text that cannot raise, not a faithful
140
+ # rendering of a peer's mojibake.
141
+ # @param text [String]
142
+ # @return [String] valid UTF-8
143
+ def decodable_text(text)
144
+ PeerText.decodable(text)
145
+ end
146
+
147
+ # A log-safe description of a JSON parse failure: where it failed and
148
+ # how much there was, never what it said.
149
+ # @param error [JSON::ParserError] the parse failure
150
+ # @param payload [Object, nil] the body that failed to parse
151
+ # @return [String]
152
+ def describe_parse_error(error, payload = nil)
153
+ parts = ['malformed JSON']
154
+ # The parser's own message quotes the bytes it choked on, so it is no
155
+ # more decodable than the body was: it is made matchable first.
156
+ location = decodable_text(error.message.to_s)[/at line \d+ column \d+/]
157
+ parts << location if location
158
+ parts << describe_body_size(payload) unless payload.nil?
159
+ parts.join(', ')
160
+ rescue StandardError
161
+ 'malformed JSON'
162
+ end
163
+
164
+ # @param body [Object, nil] a response body
165
+ # @return [String] its size, never its content
166
+ def describe_body_size(body)
167
+ text = body.to_s
168
+ text.empty? ? 'empty body' : "#{text.bytesize} byte body"
169
+ rescue StandardError
170
+ 'body of unknown size'
171
+ end
172
+ end
173
+ end
174
+ end