ruby-utcp 1.1.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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +11 -0
  3. data/LICENSE +22 -0
  4. data/Makefile +226 -0
  5. data/README.md +331 -0
  6. data/examples/basic.rb +33 -0
  7. data/examples/cli.rb +32 -0
  8. data/examples/generated/__init__.py +1 -0
  9. data/examples/generated/utcp_pb2.py +46 -0
  10. data/examples/generated/utcp_pb2_grpc.py +183 -0
  11. data/examples/graphql.rb +15 -0
  12. data/examples/grpc.rb +42 -0
  13. data/examples/grpc_python.py +52 -0
  14. data/examples/http.rb +17 -0
  15. data/examples/mcp.rb +28 -0
  16. data/examples/servers/graphql_server.rb +39 -0
  17. data/examples/servers/grpc_server.py +97 -0
  18. data/examples/servers/grpc_server.rb +62 -0
  19. data/examples/servers/http_helpers.rb +34 -0
  20. data/examples/servers/http_server.rb +28 -0
  21. data/examples/servers/mcp_stdio_server.rb +43 -0
  22. data/examples/servers/requirements-grpc.txt +2 -0
  23. data/examples/servers/sse_server.rb +36 -0
  24. data/examples/servers/streamable_http_server.rb +39 -0
  25. data/examples/servers/tcp_server.rb +58 -0
  26. data/examples/servers/udp_server.rb +33 -0
  27. data/examples/servers/webrtc_server.rb +78 -0
  28. data/examples/servers/websocket_server.rb +92 -0
  29. data/examples/sse.rb +16 -0
  30. data/examples/streamable_http.rb +17 -0
  31. data/examples/tcp.rb +20 -0
  32. data/examples/text.rb +23 -0
  33. data/examples/udp.rb +18 -0
  34. data/examples/webrtc.rb +19 -0
  35. data/examples/websocket.rb +17 -0
  36. data/lib/ruby-utcp.rb +4 -0
  37. data/lib/utcp/client.rb +217 -0
  38. data/lib/utcp/config.rb +79 -0
  39. data/lib/utcp/errors.rb +48 -0
  40. data/lib/utcp/migration.rb +88 -0
  41. data/lib/utcp/models.rb +794 -0
  42. data/lib/utcp/openapi_converter.rb +179 -0
  43. data/lib/utcp/protocols/base.rb +97 -0
  44. data/lib/utcp/protocols/cli.rb +186 -0
  45. data/lib/utcp/protocols/file.rb +52 -0
  46. data/lib/utcp/protocols/graphql.rb +277 -0
  47. data/lib/utcp/protocols/grpc.rb +207 -0
  48. data/lib/utcp/protocols/http.rb +340 -0
  49. data/lib/utcp/protocols/http_stream_support.rb +122 -0
  50. data/lib/utcp/protocols/mcp.rb +339 -0
  51. data/lib/utcp/protocols/socket_support.rb +51 -0
  52. data/lib/utcp/protocols/sse.rb +107 -0
  53. data/lib/utcp/protocols/streamable_http.rb +78 -0
  54. data/lib/utcp/protocols/tcp.rb +143 -0
  55. data/lib/utcp/protocols/text.rb +44 -0
  56. data/lib/utcp/protocols/udp.rb +61 -0
  57. data/lib/utcp/protocols/webrtc.rb +217 -0
  58. data/lib/utcp/protocols/websocket.rb +350 -0
  59. data/lib/utcp/registry.rb +67 -0
  60. data/lib/utcp/repository.rb +137 -0
  61. data/lib/utcp/serializer.rb +71 -0
  62. data/lib/utcp/utils.rb +118 -0
  63. data/lib/utcp/variables.rb +170 -0
  64. data/lib/utcp/version.rb +6 -0
  65. data/lib/utcp.rb +70 -0
  66. data/proto/utcp.proto +31 -0
  67. metadata +148 -0
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UTCP
4
+ class GraphQLProtocol < HTTPProtocol
5
+ INTROSPECTION_QUERY = <<~GRAPHQL.freeze
6
+ query UTCPIntrospection {
7
+ __schema {
8
+ queryType { name fields { name description args { name description defaultValue type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } }
9
+ mutationType { name fields { name description args { name description defaultValue type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } }
10
+ subscriptionType { name fields { name description args { name description defaultValue type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } }
11
+ types { kind name fields { name type { kind name ofType { kind name ofType { kind name } } } } enumValues { name } }
12
+ }
13
+ }
14
+ GRAPHQL
15
+
16
+ def initialize(websocket_factory: nil, **options)
17
+ super(**options)
18
+ @websocket_factory = websocket_factory || lambda do |url, headers, protocol, timeout|
19
+ WebSocketConnection.new(url, headers, protocol, timeout)
20
+ end
21
+ end
22
+
23
+ def register_manual(client, template)
24
+ assert_graphql_template!(template)
25
+ result = graphql_request(template, "query" => INTROSPECTION_QUERY, "variables" => {})
26
+ schema = result.dig("data", "__schema")
27
+ raise SerializerValidationError, "GraphQL introspection response does not contain data.__schema" unless schema
28
+
29
+ manual = Manual.new(
30
+ utcp_version: VERSION,
31
+ manual_version: "1.0.0",
32
+ info: { "title" => template.name, "version" => "1.0.0" },
33
+ tools: introspection_tools(template, schema)
34
+ )
35
+ success(template, manual)
36
+ rescue StandardError => error
37
+ client.logger.warn("Unable to register GraphQL manual #{template.name.inspect}: #{error.message}")
38
+ failure(template, error)
39
+ end
40
+
41
+ def call_tool(_client, tool_name, tool_args, template)
42
+ assert_graphql_template!(template)
43
+ if template.operation_type == "subscription"
44
+ values = []
45
+ call_subscription(tool_name, tool_args, template) { |value| values << value }
46
+ return values
47
+ end
48
+
49
+ args, headers = extract_header_arguments(template, tool_args)
50
+ payload = graphql_payload(tool_name, args, template)
51
+ response = graphql_request(template, payload, headers)
52
+ raise_graphql_errors!(response, tool_name)
53
+ extract_graphql_data(response, template.operation_name || unqualified_name(tool_name))
54
+ rescue Error
55
+ raise
56
+ rescue StandardError => error
57
+ raise ToolCallError.new("GraphQL tool #{tool_name.inspect} failed: #{error.message}", tool_name: tool_name)
58
+ end
59
+
60
+ def call_tool_streaming(_client, tool_name, tool_args, template)
61
+ return enum_for(__method__, _client, tool_name, tool_args, template) unless block_given?
62
+
63
+ if template.operation_type == "subscription"
64
+ call_subscription(tool_name, tool_args, template) { |value| yield value }
65
+ else
66
+ yield call_tool(_client, tool_name, tool_args, template)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def assert_graphql_template!(template)
73
+ return if template.is_a?(GraphQLCallTemplate)
74
+
75
+ raise ValidationError, "GraphQL protocol requires a GraphQLCallTemplate"
76
+ end
77
+
78
+ def graphql_request(template, payload, extra_headers = {})
79
+ headers = Utils.stringify_keys(template.headers || {}).merge(Utils.stringify_keys(extra_headers))
80
+ headers["Accept"] ||= "application/json"
81
+ query = {}
82
+ cookies = {}
83
+ sensitive = apply_auth(template.auth, headers, query, cookies)
84
+ if template.auth.is_a?(OAuth2Auth)
85
+ headers["Authorization"] = "Bearer #{oauth_token(template.auth)}"
86
+ sensitive << "Authorization"
87
+ end
88
+ uri = URLSecurity.validate!(append_query(template.url, query), context: "GraphQL request")
89
+ response = perform_request(
90
+ "POST", uri,
91
+ headers: headers,
92
+ cookies: cookies,
93
+ body: payload,
94
+ content_type: "application/json",
95
+ timeout: template.timeout,
96
+ sensitive_headers: sensitive.uniq
97
+ )
98
+ data = JSON.parse(response.body)
99
+ raise_graphql_errors!(data, template.operation_name || template.name)
100
+ data
101
+ rescue JSON::ParserError => error
102
+ raise SerializerValidationError, "Invalid GraphQL JSON response: #{error.message}"
103
+ end
104
+
105
+ def introspection_tools(template, schema)
106
+ types = Array(schema["types"]).each_with_object({}) { |type, map| map[type["name"]] = type if type["name"] }
107
+ %w[query mutation subscription].flat_map do |kind|
108
+ root = schema["#{kind}Type"]
109
+ next [] unless root
110
+
111
+ Array(root["fields"]).map do |field|
112
+ args = Array(field["args"])
113
+ properties = args.each_with_object({}) do |argument, result|
114
+ result[argument["name"]] = schema_for_type(argument["type"], types).tap do |value|
115
+ value["description"] = argument["description"] if argument["description"]
116
+ value["default"] = argument["defaultValue"] if argument["defaultValue"]
117
+ end
118
+ end
119
+ required = args.select { |argument| argument.dig("type", "kind") == "NON_NULL" }.map { |argument| argument["name"] }
120
+ selection = selection_for(field["type"], types)
121
+ call_template = template.to_h.merge(
122
+ "operation_type" => kind,
123
+ "operation_name" => field["name"],
124
+ "variable_types" => args.each_with_object({}) do |argument, result|
125
+ result[argument["name"]] = graphql_type_name(argument["type"])
126
+ end,
127
+ "selection_set" => selection
128
+ )
129
+ Tool.new(
130
+ name: field["name"],
131
+ description: field["description"].to_s,
132
+ inputs: Utils.compact_hash("type" => "object", "properties" => properties, "required" => required.empty? ? nil : required),
133
+ outputs: schema_for_type(field["type"], types),
134
+ tool_call_template: call_template
135
+ )
136
+ end
137
+ end
138
+ end
139
+
140
+ def schema_for_type(type, types)
141
+ return {} unless type
142
+
143
+ case type["kind"]
144
+ when "NON_NULL"
145
+ schema_for_type(type["ofType"], types)
146
+ when "LIST"
147
+ { "type" => "array", "items" => schema_for_type(type["ofType"], types) }
148
+ when "SCALAR"
149
+ { "type" => scalar_json_type(type["name"]) }
150
+ when "ENUM"
151
+ values = Array(types.dig(type["name"], "enumValues")).map { |value| value["name"] }
152
+ Utils.compact_hash("type" => "string", "enum" => values.empty? ? nil : values)
153
+ when "OBJECT", "INPUT_OBJECT", "INTERFACE", "UNION"
154
+ { "type" => "object" }
155
+ else
156
+ {}
157
+ end
158
+ end
159
+
160
+ def scalar_json_type(name)
161
+ return "integer" if name == "Int"
162
+ return "number" if name == "Float"
163
+ return "boolean" if name == "Boolean"
164
+
165
+ "string"
166
+ end
167
+
168
+ def graphql_type_name(type)
169
+ return "String" unless type
170
+
171
+ case type["kind"]
172
+ when "NON_NULL" then "#{graphql_type_name(type["ofType"])}!"
173
+ when "LIST" then "[#{graphql_type_name(type["ofType"])}]"
174
+ else type["name"] || "String"
175
+ end
176
+ end
177
+
178
+ def selection_for(type, types)
179
+ inner = type
180
+ inner = inner["ofType"] while inner && %w[NON_NULL LIST].include?(inner["kind"])
181
+ return nil unless inner && %w[OBJECT INTERFACE UNION].include?(inner["kind"])
182
+
183
+ fields = Array(types.dig(inner["name"], "fields")).select do |field|
184
+ child = field["type"]
185
+ child = child["ofType"] while child && child["kind"] == "NON_NULL"
186
+ child && %w[SCALAR ENUM].include?(child["kind"])
187
+ end.map { |field| field["name"] }.first(20)
188
+ fields.empty? ? "__typename" : fields.join(" ")
189
+ end
190
+
191
+ def graphql_payload(tool_name, arguments, template)
192
+ return { "query" => template.query, "variables" => arguments } if template.query
193
+
194
+ field = template.operation_name || unqualified_name(tool_name)
195
+ variable_types = arguments.each_with_object({}) do |(name, _value), result|
196
+ result[name] = template.variable_types[name] || "String"
197
+ end
198
+ declarations = variable_types.map { |name, type| "$#{name}: #{type}" }.join(", ")
199
+ invocation = variable_types.keys.map { |name| "#{name}: $#{name}" }.join(", ")
200
+ operation_label = "UTCP_#{field.gsub(/[^A-Za-z0-9_]/, "_")}"
201
+ query = +"#{template.operation_type} #{operation_label}"
202
+ query << "(#{declarations})" unless declarations.empty?
203
+ query << " { #{field}"
204
+ query << "(#{invocation})" unless invocation.empty?
205
+ query << " { #{template.selection_set} }" if template.selection_set && !template.selection_set.empty?
206
+ query << " }"
207
+ { "query" => query, "variables" => arguments, "operationName" => operation_label }
208
+ end
209
+
210
+ def extract_header_arguments(template, arguments)
211
+ args = Utils.stringify_keys(arguments || {})
212
+ headers = {}
213
+ template.header_fields.each { |field| headers[field] = args.delete(field).to_s if args.key?(field) }
214
+ [args, headers]
215
+ end
216
+
217
+ def raise_graphql_errors!(response, tool_name)
218
+ errors = response["errors"] if response.is_a?(Hash)
219
+ return if errors.nil? || errors.empty?
220
+
221
+ detail = errors.map { |error| error.is_a?(Hash) ? error["message"] : error.to_s }.join("; ")
222
+ raise ToolCallError.new("GraphQL tool #{tool_name.inspect} returned errors: #{detail}", tool_name: tool_name,
223
+ response_body: JSON.generate(errors))
224
+ end
225
+
226
+ def extract_graphql_data(response, field)
227
+ data = response["data"]
228
+ data.is_a?(Hash) && data.key?(field) ? data[field] : data
229
+ end
230
+
231
+ def unqualified_name(tool_name)
232
+ tool_name.to_s.split(".").last
233
+ end
234
+
235
+ def call_subscription(tool_name, tool_args, template)
236
+ args, headers = extract_header_arguments(template, tool_args)
237
+ query = {}
238
+ cookies = {}
239
+ apply_auth(template.auth, headers, query, cookies)
240
+ headers["Authorization"] = "Bearer #{oauth_token(template.auth)}" if template.auth.is_a?(OAuth2Auth)
241
+ headers["Cookie"] = cookies.map { |key, value| "#{key}=#{value}" }.join("; ") unless cookies.empty?
242
+ http_uri = URLSecurity.validate!(append_query(template.url, query), context: "GraphQL subscription")
243
+ ws_scheme = http_uri.scheme == "https" ? "wss" : "ws"
244
+ ws_url = http_uri.to_s.sub(/\Ahttps?/, ws_scheme)
245
+ connection = @websocket_factory.call(ws_url, headers, "graphql-transport-ws", template.timeout)
246
+ identifier = SecureRandom.uuid
247
+ connection.send_text(JSON.generate("type" => "connection_init", "payload" => {}))
248
+ ack = connection.read_message
249
+ ack_data = ack && decode_json_or_text(ack[1].force_encoding(Encoding::UTF_8))
250
+ unless ack_data.is_a?(Hash) && ack_data["type"] == "connection_ack"
251
+ raise ToolCallError, "GraphQL subscription did not receive connection_ack"
252
+ end
253
+ connection.send_text(JSON.generate(
254
+ "id" => identifier,
255
+ "type" => "subscribe",
256
+ "payload" => graphql_payload(tool_name, args, template)
257
+ ))
258
+ while (frame = connection.read_message)
259
+ message = decode_json_or_text(frame[1].force_encoding(Encoding::UTF_8))
260
+ next unless message.is_a?(Hash) && message["id"] == identifier
261
+ break if message["type"] == "complete"
262
+ if message["type"] == "error"
263
+ raise ToolCallError, "GraphQL subscription failed: #{message["payload"].inspect}"
264
+ end
265
+ next unless message["type"] == "next"
266
+
267
+ payload = message["payload"] || {}
268
+ raise_graphql_errors!(payload, tool_name)
269
+ yield extract_graphql_data(payload, template.operation_name || unqualified_name(tool_name))
270
+ end
271
+ ensure
272
+ connection.close if defined?(connection) && connection
273
+ end
274
+ end
275
+ GraphqlCommunicationProtocol = GraphQLProtocol
276
+ GraphQLCommunicationProtocol = GraphQLProtocol
277
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UTCP
4
+ module ProtobufWire
5
+ module_function
6
+
7
+ def string_field(number, value)
8
+ bytes = value.to_s.b
9
+ varint((number << 3) | 2) + varint(bytes.bytesize) + bytes
10
+ end
11
+
12
+ def varint(value)
13
+ number = Integer(value)
14
+ result = +"".b
15
+ loop do
16
+ byte = number & 0x7F
17
+ number >>= 7
18
+ byte |= 0x80 unless number.zero?
19
+ result << byte
20
+ break if number.zero?
21
+ end
22
+ result
23
+ end
24
+
25
+ def fields(bytes)
26
+ data = bytes.to_s.b
27
+ offset = 0
28
+ result = Hash.new { |hash, key| hash[key] = [] }
29
+ while offset < data.bytesize
30
+ tag, offset = read_varint(data, offset)
31
+ number = tag >> 3
32
+ wire = tag & 7
33
+ case wire
34
+ when 0
35
+ value, offset = read_varint(data, offset)
36
+ when 1
37
+ value = data.byteslice(offset, 8)
38
+ offset += 8
39
+ when 2
40
+ length, offset = read_varint(data, offset)
41
+ raise SerializerValidationError, "truncated protobuf field" if offset + length > data.bytesize
42
+ value = data.byteslice(offset, length)
43
+ offset += length
44
+ when 5
45
+ value = data.byteslice(offset, 4)
46
+ offset += 4
47
+ else
48
+ raise SerializerValidationError, "unsupported protobuf wire type #{wire}"
49
+ end
50
+ result[number] << value
51
+ end
52
+ result
53
+ end
54
+
55
+ def read_varint(data, offset)
56
+ value = 0
57
+ shift = 0
58
+ loop do
59
+ raise SerializerValidationError, "truncated protobuf varint" if offset >= data.bytesize
60
+ byte = data.getbyte(offset)
61
+ offset += 1
62
+ value |= (byte & 0x7F) << shift
63
+ return [value, offset] if (byte & 0x80).zero?
64
+ shift += 7
65
+ raise SerializerValidationError, "protobuf varint is too long" if shift > 63
66
+ end
67
+ end
68
+ end
69
+
70
+ class GRPCGemClient
71
+ def initialize(template)
72
+ require "grpc"
73
+ address = "#{template.host}:#{template.port}"
74
+ credentials = template.use_ssl ? GRPC::Core::ChannelCredentials.new : :this_channel_is_insecure
75
+ @stub = GRPC::ClientStub.new(address, credentials)
76
+ rescue LoadError => error
77
+ raise MissingDependencyError,
78
+ "gRPC requires the optional 'grpc' gem (add gem \"grpc\" to your Gemfile): #{error.message}"
79
+ end
80
+
81
+ def unary(route, payload, timeout:, metadata: {})
82
+ @stub.request_response(
83
+ route, payload,
84
+ ->(value) { value.to_s.b }, ->(bytes) { bytes },
85
+ deadline: Time.now + timeout,
86
+ metadata: metadata
87
+ )
88
+ end
89
+
90
+ def server_stream(route, payload, timeout:, metadata: {})
91
+ @stub.server_streamer(
92
+ route, payload,
93
+ ->(value) { value.to_s.b }, ->(bytes) { bytes },
94
+ deadline: Time.now + timeout,
95
+ metadata: metadata
96
+ )
97
+ end
98
+ end
99
+
100
+ class GRPCProtocol < HTTPProtocol
101
+ def initialize(rpc_client_factory: nil, **options)
102
+ super(**options)
103
+ @rpc_client_factory = rpc_client_factory || ->(template) { GRPCGemClient.new(template) }
104
+ end
105
+
106
+ def register_manual(client, template)
107
+ assert_grpc_template!(template)
108
+ response = rpc_client(template).unary(
109
+ route(template, "GetManual"), "".b,
110
+ timeout: template.timeout,
111
+ metadata: grpc_metadata(template)
112
+ )
113
+ fields = ProtobufWire.fields(response)
114
+ tools = fields[2].map do |tool_bytes|
115
+ tool_fields = ProtobufWire.fields(tool_bytes)
116
+ {
117
+ "name" => tool_fields[1].first.to_s.force_encoding(Encoding::UTF_8),
118
+ "description" => tool_fields[2].first.to_s.force_encoding(Encoding::UTF_8),
119
+ "tool_call_template" => template.to_h
120
+ }
121
+ end
122
+ manual = Manual.new(
123
+ utcp_version: VERSION,
124
+ manual_version: fields[1].first.to_s.empty? ? "1.0.0" : fields[1].first.to_s,
125
+ tools: tools
126
+ )
127
+ success(template, manual)
128
+ rescue StandardError => error
129
+ client.logger.warn("Unable to register gRPC manual #{template.name.inspect}: #{error.message}")
130
+ failure(template, error)
131
+ end
132
+
133
+ def call_tool(_client, tool_name, tool_args, template)
134
+ assert_grpc_template!(template)
135
+ response = rpc_client(template).unary(
136
+ route(template, template.method_name || "CallTool"),
137
+ tool_call_request(tool_name, tool_args),
138
+ timeout: template.timeout,
139
+ metadata: grpc_metadata(template)
140
+ )
141
+ decode_tool_response(response)
142
+ rescue Error
143
+ raise
144
+ rescue StandardError => error
145
+ raise ToolCallError.new("gRPC tool #{tool_name.inspect} failed: #{error.message}", tool_name: tool_name)
146
+ end
147
+
148
+ def call_tool_streaming(_client, tool_name, tool_args, template)
149
+ return enum_for(__method__, _client, tool_name, tool_args, template) unless block_given?
150
+
151
+ assert_grpc_template!(template)
152
+ method = template.method_name || "CallToolStream"
153
+ rpc_client(template).server_stream(
154
+ route(template, method),
155
+ tool_call_request(tool_name, tool_args),
156
+ timeout: template.timeout,
157
+ metadata: grpc_metadata(template)
158
+ ).each { |response| yield decode_tool_response(response) }
159
+ rescue Error
160
+ raise
161
+ rescue StandardError => error
162
+ raise ToolCallError.new("streaming gRPC tool #{tool_name.inspect} failed: #{error.message}", tool_name: tool_name)
163
+ end
164
+
165
+ private
166
+
167
+ def assert_grpc_template!(template)
168
+ return if template.is_a?(GrpcCallTemplate)
169
+
170
+ raise ValidationError, "gRPC protocol requires a GrpcCallTemplate"
171
+ end
172
+
173
+ def rpc_client(template)
174
+ @rpc_client_factory.call(template)
175
+ end
176
+
177
+ def route(template, method)
178
+ "/#{template.service_name}/#{method}"
179
+ end
180
+
181
+ def tool_call_request(tool_name, tool_args)
182
+ ProtobufWire.string_field(1, tool_name) +
183
+ ProtobufWire.string_field(2, JSON.generate(Utils.stringify_keys(tool_args || {})))
184
+ end
185
+
186
+ def decode_tool_response(bytes)
187
+ json = ProtobufWire.fields(bytes)[1].first.to_s
188
+ json.empty? ? nil : decode_json_or_text(json.force_encoding(Encoding::UTF_8))
189
+ end
190
+
191
+ def grpc_metadata(template)
192
+ values = Utils.stringify_keys(template.metadata || {})
193
+ values["target"] = template.target if template.target
194
+ case template.auth
195
+ when ApiKeyAuth
196
+ values[template.auth.var_name.downcase] = template.auth.api_key
197
+ when BasicAuth
198
+ values["authorization"] = "Basic #{Base64.strict_encode64("#{template.auth.username}:#{template.auth.password}")}"
199
+ when OAuth2Auth
200
+ values["authorization"] = "Bearer #{oauth_token(template.auth)}"
201
+ end
202
+ values
203
+ end
204
+ end
205
+ GrpcCommunicationProtocol = GRPCProtocol
206
+ GRPCCommunicationProtocol = GRPCProtocol
207
+ end