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
data/lib/utcp/utils.rb ADDED
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+
6
+ module UTCP
7
+ module ModelSerialization
8
+ def to_json(*arguments)
9
+ to_h.to_json(*arguments)
10
+ end
11
+
12
+ def ==(other)
13
+ other.instance_of?(self.class) && other.to_h == to_h
14
+ end
15
+ alias eql? ==
16
+
17
+ def hash
18
+ [self.class, to_h].hash
19
+ end
20
+ end
21
+
22
+ module Utils
23
+ module_function
24
+
25
+ def stringify_keys(value)
26
+ case value
27
+ when Hash
28
+ value.each_with_object({}) { |(key, item), copy| copy[key.to_s] = stringify_keys(item) }
29
+ when Array
30
+ value.map { |item| stringify_keys(item) }
31
+ else
32
+ value
33
+ end
34
+ end
35
+
36
+ def symbolize_keys(value)
37
+ value.each_with_object({}) { |(key, item), copy| copy[key.to_sym] = item }
38
+ end
39
+
40
+ def deep_copy(value)
41
+ case value
42
+ when Hash
43
+ value.each_with_object({}) { |(key, item), copy| copy[key] = deep_copy(item) }
44
+ when Array
45
+ value.map { |item| deep_copy(item) }
46
+ when String
47
+ value.dup
48
+ else
49
+ value
50
+ end
51
+ end
52
+
53
+ def compact_hash(hash)
54
+ hash.each_with_object({}) do |(key, value), result|
55
+ result[key] = value unless value.nil?
56
+ end
57
+ end
58
+
59
+ def required_string!(value, field)
60
+ return value if value.is_a?(String) && !value.empty?
61
+
62
+ raise ValidationError.new("must be a non-empty String", path: field)
63
+ end
64
+
65
+ def optional_string!(value, field)
66
+ return nil if value.nil?
67
+ return value if value.is_a?(String)
68
+
69
+ raise ValidationError.new("must be a String", path: field)
70
+ end
71
+
72
+ def hash!(value, field)
73
+ return value if value.is_a?(Hash)
74
+
75
+ raise ValidationError.new("must be an object", path: field)
76
+ end
77
+
78
+ def array!(value, field)
79
+ return value if value.is_a?(Array)
80
+
81
+ raise ValidationError.new("must be an array", path: field)
82
+ end
83
+
84
+ def parse_document(content, source: nil)
85
+ JSON.parse(content)
86
+ rescue JSON::ParserError => json_error
87
+ begin
88
+ parsed = YAML.safe_load(content, permitted_classes: [], permitted_symbols: [], aliases: false)
89
+ raise ValidationError, "document is empty" if parsed.nil?
90
+
91
+ parsed
92
+ rescue Psych::Exception, ValidationError => yaml_error
93
+ label = source ? " #{source}" : ""
94
+ raise SerializerValidationError,
95
+ "Could not parse#{label} as JSON or safe YAML: #{json_error.message}; #{yaml_error.message}"
96
+ end
97
+ end
98
+
99
+ def load_document(path)
100
+ parse_document(File.read(path, mode: "r:bom|utf-8"), source: path)
101
+ rescue Errno::ENOENT => error
102
+ raise ValidationError, "File not found: #{path} (#{error.message})"
103
+ rescue Errno::EACCES => error
104
+ raise ValidationError, "Cannot read file: #{path} (#{error.message})"
105
+ end
106
+
107
+ def json_value(value)
108
+ case value
109
+ when String, Numeric, TrueClass, FalseClass, NilClass
110
+ value
111
+ when Hash, Array
112
+ value
113
+ else
114
+ value.to_s
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UTCP
4
+ class VariableLoader
5
+ attr_reader :variable_loader_type
6
+
7
+ def self.from_h(value, root_dir: Dir.pwd)
8
+ return value if value.respond_to?(:get)
9
+
10
+ data = Utils.stringify_keys(Utils.hash!(value, "variable loader"))
11
+ case data["variable_loader_type"]
12
+ when "dotenv"
13
+ DotenvVariableLoader.new(
14
+ env_file_path: data["env_file_path"] || ".env",
15
+ root_dir: root_dir
16
+ )
17
+ else
18
+ raise ValidationError.new(
19
+ "unsupported variable loader type #{data['variable_loader_type'].inspect}",
20
+ path: "variable_loader_type"
21
+ )
22
+ end
23
+ end
24
+
25
+ def initialize(variable_loader_type)
26
+ @variable_loader_type = variable_loader_type
27
+ end
28
+
29
+ def get(_key)
30
+ raise NotImplementedError
31
+ end
32
+ end
33
+
34
+ class DotenvVariableLoader < VariableLoader
35
+ attr_reader :env_file_path
36
+
37
+ def initialize(env_file_path: ".env", root_dir: Dir.pwd)
38
+ super("dotenv")
39
+ @env_file_path = File.expand_path(env_file_path, root_dir)
40
+ @variables = nil
41
+ @mutex = Mutex.new
42
+ end
43
+
44
+ def get(key)
45
+ load_variables.fetch(key.to_s, nil)
46
+ end
47
+
48
+ def reload!
49
+ @mutex.synchronize { @variables = nil }
50
+ self
51
+ end
52
+
53
+ def to_h
54
+ { "variable_loader_type" => variable_loader_type, "env_file_path" => env_file_path }
55
+ end
56
+
57
+ private
58
+
59
+ def load_variables
60
+ @mutex.synchronize do
61
+ @variables ||= parse_file
62
+ end
63
+ end
64
+
65
+ def parse_file
66
+ return {} unless File.file?(env_file_path)
67
+
68
+ File.readlines(env_file_path, chomp: true).each_with_object({}) do |line, values|
69
+ stripped = line.strip
70
+ next if stripped.empty? || stripped.start_with?("#")
71
+
72
+ stripped = stripped.sub(/\Aexport\s+/, "")
73
+ key, raw = stripped.split("=", 2)
74
+ next unless raw && key.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
75
+
76
+ values[key] = parse_value(raw.strip)
77
+ end
78
+ end
79
+
80
+ def parse_value(raw)
81
+ if raw.length >= 2 && raw.start_with?("'") && raw.end_with?("'")
82
+ raw[1..-2]
83
+ elsif raw.length >= 2 && raw.start_with?("\"") && raw.end_with?("\"")
84
+ raw[1..-2].gsub(/\\n/, "\n").gsub(/\\r/, "\r").gsub(/\\t/, "\t").gsub(/\\\"/, "\"")
85
+ else
86
+ raw.sub(/\s+#.*\z/, "")
87
+ end
88
+ end
89
+ end
90
+
91
+ class VariableSubstitutor
92
+ VARIABLE_PATTERN = /\$\{([A-Za-z0-9_]+)\}|\$([A-Za-z0-9_]+)/.freeze
93
+ JSON_REF_PATTERN = /\$ref(?![A-Za-z0-9_])/.freeze
94
+
95
+ def substitute(value, config, namespace = nil)
96
+ validate_namespace!(namespace)
97
+ case value
98
+ when String
99
+ return value if value.match?(JSON_REF_PATTERN)
100
+
101
+ value.gsub(VARIABLE_PATTERN) do
102
+ key = Regexp.last_match(1) || Regexp.last_match(2)
103
+ next Regexp.last_match(0) if key.match?(/\ACMD_\d+_OUTPUT\z/)
104
+
105
+ resolve(key, config, namespace)
106
+ end
107
+ when Hash
108
+ value.each_with_object({}) do |(key, item), result|
109
+ result[key] = substitute(item, config, namespace)
110
+ end
111
+ when Array
112
+ value.map { |item| substitute(item, config, namespace) }
113
+ else
114
+ value
115
+ end
116
+ end
117
+
118
+ def find_required_variables(value, namespace = nil)
119
+ validate_namespace!(namespace)
120
+ names = collect_required_variables(value)
121
+ names.map { |name| namespace ? "#{encoded_namespace(namespace)}_#{name}" : name }.uniq
122
+ end
123
+
124
+ private
125
+
126
+ def resolve(key, config, namespace)
127
+ candidates = namespace ? ["#{encoded_namespace(namespace)}_#{key}", key] : [key]
128
+ candidates.each do |candidate|
129
+ direct = config.variables[candidate]
130
+ return direct.to_s unless direct.nil?
131
+
132
+ config.load_variables_from.each do |loader|
133
+ loaded = loader.get(candidate)
134
+ return loaded.to_s unless loaded.nil?
135
+ end
136
+
137
+ environment = ENV[candidate]
138
+ return environment unless environment.nil?
139
+ end
140
+
141
+ raise VariableNotFoundError, candidates.first
142
+ end
143
+
144
+ def collect_required_variables(value)
145
+ case value
146
+ when String
147
+ return [] if value.match?(JSON_REF_PATTERN)
148
+
149
+ value.scan(VARIABLE_PATTERN).map { |groups| groups.compact.first }
150
+ .reject { |name| name.match?(/\ACMD_\d+_OUTPUT\z/) }
151
+ when Hash
152
+ value.values.flat_map { |item| collect_required_variables(item) }
153
+ when Array
154
+ value.flat_map { |item| collect_required_variables(item) }
155
+ else
156
+ []
157
+ end
158
+ end
159
+
160
+ def encoded_namespace(namespace)
161
+ namespace.to_s.gsub("_", "__")
162
+ end
163
+
164
+ def validate_namespace!(namespace)
165
+ return if namespace.nil? || namespace.to_s.match?(/\A[[:alnum:]_]+\z/)
166
+
167
+ raise ArgumentError, "variable namespace may only contain letters, numbers, and underscores"
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UTCP
4
+ VERSION = "1.1.0"
5
+ end
6
+
data/lib/utcp.rb ADDED
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "utcp/version"
4
+ require_relative "utcp/errors"
5
+ require_relative "utcp/utils"
6
+ require_relative "utcp/registry"
7
+ require_relative "utcp/models"
8
+ require_relative "utcp/repository"
9
+ require_relative "utcp/variables"
10
+ require_relative "utcp/config"
11
+ require_relative "utcp/serializer"
12
+ require_relative "utcp/protocols/base"
13
+ require_relative "utcp/openapi_converter"
14
+ require_relative "utcp/migration"
15
+ require_relative "utcp/protocols/http"
16
+ require_relative "utcp/protocols/http_stream_support"
17
+ require_relative "utcp/protocols/sse"
18
+ require_relative "utcp/protocols/streamable_http"
19
+ require_relative "utcp/protocols/cli"
20
+ require_relative "utcp/protocols/text"
21
+ require_relative "utcp/protocols/file"
22
+ require_relative "utcp/protocols/websocket"
23
+ require_relative "utcp/protocols/graphql"
24
+ require_relative "utcp/protocols/grpc"
25
+ require_relative "utcp/protocols/socket_support"
26
+ require_relative "utcp/protocols/tcp"
27
+ require_relative "utcp/protocols/udp"
28
+ require_relative "utcp/protocols/mcp"
29
+ require_relative "utcp/protocols/webrtc"
30
+ require_relative "utcp/client"
31
+
32
+ module UTCP
33
+ register_auth("api_key", ApiKeyAuth)
34
+ register_auth("basic", BasicAuth)
35
+ register_auth("oauth2", OAuth2Auth)
36
+
37
+ register_call_template("http", HttpCallTemplate)
38
+ register_call_template("sse", SseCallTemplate)
39
+ register_call_template("streamable_http", StreamableHttpCallTemplate)
40
+ register_call_template("http_stream", StreamableHttpCallTemplate)
41
+ register_call_template("cli", CliCallTemplate)
42
+ register_call_template("websocket", WebSocketCallTemplate)
43
+ register_call_template("grpc", GrpcCallTemplate)
44
+ register_call_template("graphql", GraphQLCallTemplate)
45
+ register_call_template("tcp", TcpCallTemplate)
46
+ register_call_template("udp", UdpCallTemplate)
47
+ register_call_template("webrtc", WebRtcCallTemplate)
48
+ register_call_template("mcp", McpCallTemplate)
49
+ register_call_template("text", TextCallTemplate)
50
+ register_call_template("file", FileCallTemplate)
51
+
52
+ register_protocol("http", HTTPProtocol.new)
53
+ register_protocol("sse", SSEProtocol.new)
54
+ streamable_http_protocol = StreamableHTTPProtocol.new
55
+ register_protocol("streamable_http", streamable_http_protocol)
56
+ register_protocol("http_stream", streamable_http_protocol)
57
+ register_protocol("cli", CLIProtocol.new)
58
+ register_protocol("websocket", WebSocketProtocol.new)
59
+ register_protocol("grpc", GRPCProtocol.new)
60
+ register_protocol("graphql", GraphQLProtocol.new)
61
+ register_protocol("tcp", TCPProtocol.new)
62
+ register_protocol("udp", UDPProtocol.new)
63
+ register_protocol("webrtc", WebRTCProtocol.new)
64
+ register_protocol("mcp", MCPProtocol.new)
65
+ register_protocol("text", TextProtocol.new)
66
+ register_protocol("file", FileProtocol.new)
67
+ end
68
+
69
+ # Conventional Ruby casing alias for applications that do not preserve acronyms.
70
+ Utcp = UTCP unless defined?(Utcp)
data/proto/utcp.proto ADDED
@@ -0,0 +1,31 @@
1
+ syntax = "proto3";
2
+
3
+ package grpcpb;
4
+
5
+ // Minimal UTCP gRPC contract used by the Ruby client and example servers.
6
+ message Empty {}
7
+
8
+ message Tool {
9
+ string name = 1;
10
+ string description = 2;
11
+ }
12
+
13
+ message Manual {
14
+ string version = 1;
15
+ repeated Tool tools = 2;
16
+ }
17
+
18
+ message ToolCallRequest {
19
+ string tool = 1;
20
+ string args_json = 2;
21
+ }
22
+
23
+ message ToolCallResponse {
24
+ string result_json = 1;
25
+ }
26
+
27
+ service UTCPService {
28
+ rpc GetManual(Empty) returns (Manual);
29
+ rpc CallTool(ToolCallRequest) returns (ToolCallResponse);
30
+ rpc CallToolStream(ToolCallRequest) returns (stream ToolCallResponse);
31
+ }
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-utcp
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ruby-utcp contributors
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '5.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '7.0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '5.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '7.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: rake
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '12.0'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '14.0'
42
+ type: :development
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '12.0'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '14.0'
52
+ description: Discover, search, and call UTCP 1.1 tools over pluggable native protocols.
53
+ email:
54
+ - maintainers@utcp.io
55
+ executables: []
56
+ extensions: []
57
+ extra_rdoc_files: []
58
+ files:
59
+ - CHANGELOG.md
60
+ - LICENSE
61
+ - Makefile
62
+ - README.md
63
+ - examples/basic.rb
64
+ - examples/cli.rb
65
+ - examples/generated/__init__.py
66
+ - examples/generated/utcp_pb2.py
67
+ - examples/generated/utcp_pb2_grpc.py
68
+ - examples/graphql.rb
69
+ - examples/grpc.rb
70
+ - examples/grpc_python.py
71
+ - examples/http.rb
72
+ - examples/mcp.rb
73
+ - examples/servers/graphql_server.rb
74
+ - examples/servers/grpc_server.py
75
+ - examples/servers/grpc_server.rb
76
+ - examples/servers/http_helpers.rb
77
+ - examples/servers/http_server.rb
78
+ - examples/servers/mcp_stdio_server.rb
79
+ - examples/servers/requirements-grpc.txt
80
+ - examples/servers/sse_server.rb
81
+ - examples/servers/streamable_http_server.rb
82
+ - examples/servers/tcp_server.rb
83
+ - examples/servers/udp_server.rb
84
+ - examples/servers/webrtc_server.rb
85
+ - examples/servers/websocket_server.rb
86
+ - examples/sse.rb
87
+ - examples/streamable_http.rb
88
+ - examples/tcp.rb
89
+ - examples/text.rb
90
+ - examples/udp.rb
91
+ - examples/webrtc.rb
92
+ - examples/websocket.rb
93
+ - lib/ruby-utcp.rb
94
+ - lib/utcp.rb
95
+ - lib/utcp/client.rb
96
+ - lib/utcp/config.rb
97
+ - lib/utcp/errors.rb
98
+ - lib/utcp/migration.rb
99
+ - lib/utcp/models.rb
100
+ - lib/utcp/openapi_converter.rb
101
+ - lib/utcp/protocols/base.rb
102
+ - lib/utcp/protocols/cli.rb
103
+ - lib/utcp/protocols/file.rb
104
+ - lib/utcp/protocols/graphql.rb
105
+ - lib/utcp/protocols/grpc.rb
106
+ - lib/utcp/protocols/http.rb
107
+ - lib/utcp/protocols/http_stream_support.rb
108
+ - lib/utcp/protocols/mcp.rb
109
+ - lib/utcp/protocols/socket_support.rb
110
+ - lib/utcp/protocols/sse.rb
111
+ - lib/utcp/protocols/streamable_http.rb
112
+ - lib/utcp/protocols/tcp.rb
113
+ - lib/utcp/protocols/text.rb
114
+ - lib/utcp/protocols/udp.rb
115
+ - lib/utcp/protocols/webrtc.rb
116
+ - lib/utcp/protocols/websocket.rb
117
+ - lib/utcp/registry.rb
118
+ - lib/utcp/repository.rb
119
+ - lib/utcp/serializer.rb
120
+ - lib/utcp/utils.rb
121
+ - lib/utcp/variables.rb
122
+ - lib/utcp/version.rb
123
+ - proto/utcp.proto
124
+ homepage: https://www.utcp.io
125
+ licenses:
126
+ - MIT
127
+ metadata:
128
+ homepage_uri: https://www.utcp.io
129
+ source_code_uri: https://github.com/universal-tool-calling-protocol/ruby-utcp
130
+ documentation_uri: https://www.utcp.io/implementation
131
+ rdoc_options: []
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: 2.6.0
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubygems_version: 4.0.20
146
+ specification_version: 4
147
+ summary: A Ruby implementation of the Universal Tool Calling Protocol
148
+ test_files: []