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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4a695abfcf37112217be77067218db124af0ea02490ea1811ecafc5f6cf2ab30
4
+ data.tar.gz: 8ef4eb2d484d370820f9c10cfc95c00c36e66675770e49ac848790ba3bf1678c
5
+ SHA512:
6
+ metadata.gz: 703e659df46fcd3574bd377debd7623b808e4b57334da07a9a6a28461ad25ad5468b97e81aff53b066178de43184f24117a0ccd7ab75d97241cbaabf0dd26574
7
+ data.tar.gz: 0661067b9c78f93f140a130d7e4d21dc1c1debe50e0dcbfcddee1c70e5bb67186b14d4a57433a29b61505fed4e85dcf77a1a0dfd38a7cb669fb784867bccdf36
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 1.1.0
4
+
5
+ - Initial Ruby implementation of the UTCP 1.1 client and data model.
6
+ - All 12 reference transports: HTTP, SSE, Streamable HTTP, CLI, WebSocket, gRPC,
7
+ GraphQL, TCP, UDP, WebRTC, MCP, and text; plus a Ruby file extension.
8
+ - Streaming enumerators, GraphQL introspection/subscriptions, socket framing,
9
+ MCP stdio/HTTP sessions, RFC 6455 WebSockets, and optional native gRPC/WebRTC backends.
10
+ - Secure-by-default `allowed_communication_protocols` enforcement.
11
+ - Migration helpers for UTCP v0.1 configuration and manuals.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ruby-utcp contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/Makefile ADDED
@@ -0,0 +1,226 @@
1
+ SHELL := /bin/bash
2
+ .DEFAULT_GOAL := demo
3
+
4
+ RUBY ?= ruby
5
+ RUBY_RUN := $(RUBY) -Ilib
6
+ PYTHON ?= python3
7
+ PYTHON_GRPC_PORT ?= 50051
8
+ PYTHON_GRPC := $(if $(wildcard .venv-grpc/bin/python),.venv-grpc/bin/python,$(PYTHON))
9
+
10
+ STANDARD_SERVER_SCRIPTS := \
11
+ examples/servers/http_server.rb \
12
+ examples/servers/sse_server.rb \
13
+ examples/servers/streamable_http_server.rb \
14
+ examples/servers/websocket_server.rb \
15
+ examples/servers/graphql_server.rb \
16
+ examples/servers/tcp_server.rb \
17
+ examples/servers/udp_server.rb
18
+
19
+ STANDARD_CLIENT_SCRIPTS := \
20
+ examples/http.rb \
21
+ examples/sse.rb \
22
+ examples/streamable_http.rb \
23
+ examples/cli.rb \
24
+ examples/websocket.rb \
25
+ examples/graphql.rb \
26
+ examples/tcp.rb \
27
+ examples/udp.rb \
28
+ examples/mcp.rb \
29
+ examples/text.rb
30
+
31
+ GRPC_AVAILABLE := $(shell $(RUBY) -e 'require "grpc"; print "yes"' 2>/dev/null)
32
+ WEBRTC_AVAILABLE := $(shell $(RUBY) -e 'gem "webrtc-ruby", ">= 1.0.0"; require "webrtc"; abort unless defined?(WebRTC::RTCPeerConnection); print "yes"' 2>/dev/null)
33
+
34
+ AVAILABLE_SERVER_SCRIPTS := $(STANDARD_SERVER_SCRIPTS)
35
+ AVAILABLE_CLIENT_SCRIPTS := $(STANDARD_CLIENT_SCRIPTS)
36
+ AVAILABLE_PORTS := 8080 8081 8082 8083 8085 9000
37
+
38
+ ifneq ($(DISABLE_OPTIONAL),1)
39
+ ifeq ($(GRPC_AVAILABLE),yes)
40
+ AVAILABLE_SERVER_SCRIPTS += examples/servers/grpc_server.rb
41
+ AVAILABLE_CLIENT_SCRIPTS += examples/grpc.rb
42
+ AVAILABLE_PORTS += 50051
43
+ endif
44
+ ifeq ($(WEBRTC_AVAILABLE),yes)
45
+ AVAILABLE_SERVER_SCRIPTS += examples/servers/webrtc_server.rb
46
+ AVAILABLE_CLIENT_SCRIPTS += examples/webrtc.rb
47
+ AVAILABLE_PORTS += 8084
48
+ endif
49
+ endif
50
+
51
+ .PHONY: all demo full-demo servers full-servers examples full-examples \
52
+ check-base-dependencies check-ruby-grpc check-python-grpc check-python-grpc-tools \
53
+ check-dependencies dependency-report grpc-python-setup grpc-python-generate \
54
+ grpc-python-server grpc-python-client grpc-python-demo test standard-demo
55
+
56
+ all: demo
57
+
58
+ check-base-dependencies:
59
+ @$(RUBY) -e 'require "webrick"' || { \
60
+ echo "WEBrick is required by the local HTTP example servers." >&2; \
61
+ echo "Install it with: $$(ruby -e 'print RbConfig.ruby') -S gem install webrick" >&2; \
62
+ exit 1; \
63
+ }
64
+
65
+ check-ruby-grpc:
66
+ @$(RUBY) -e 'require "grpc"' || { \
67
+ echo "The Ruby gRPC client requires the grpc gem." >&2; \
68
+ echo "Install it with Ruby 3.1+: gem install grpc" >&2; \
69
+ exit 1; \
70
+ }
71
+
72
+ check-python-grpc:
73
+ @$(PYTHON_GRPC) -c 'import grpc' || { \
74
+ echo "The Python gRPC server requires grpcio." >&2; \
75
+ echo "Install them with: make grpc-python-setup" >&2; \
76
+ exit 1; \
77
+ }
78
+
79
+ check-python-grpc-tools: check-python-grpc
80
+ @$(PYTHON_GRPC) -c 'from grpc_tools import protoc' || { \
81
+ echo "Stub generation requires grpcio-tools." >&2; \
82
+ echo "Install it with: make grpc-python-setup" >&2; \
83
+ exit 1; \
84
+ }
85
+
86
+ check-dependencies: check-base-dependencies check-ruby-grpc
87
+ @$(RUBY) -e 'abort "Ruby 3.1+ is required by webrtc-ruby (current: #{RUBY_VERSION})" if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.1"); gem "webrtc-ruby", ">= 1.0.0"; require "webrtc"; abort "webrtc-ruby did not load WebRTC::RTCPeerConnection" unless defined?(WebRTC::RTCPeerConnection)' || { \
88
+ echo "Install webrtc-ruby and libdatachannel before running the full demo." >&2; \
89
+ exit 1; \
90
+ }
91
+
92
+ dependency-report:
93
+ @if [[ "$(DISABLE_OPTIONAL)" != "1" && "$(GRPC_AVAILABLE)" != "yes" ]]; then \
94
+ echo "[skip] gRPC example: install the grpc gem (Ruby 3.1+ recommended)"; \
95
+ fi
96
+ @if [[ "$(DISABLE_OPTIONAL)" != "1" && "$(WEBRTC_AVAILABLE)" != "yes" ]]; then \
97
+ echo "[skip] WebRTC example: requires Ruby 3.1+, webrtc-ruby, and libdatachannel"; \
98
+ fi
99
+
100
+ servers: check-base-dependencies dependency-report
101
+ @set -euo pipefail; \
102
+ pids=(); \
103
+ cleanup() { \
104
+ if (($${#pids[@]})); then \
105
+ kill "$${pids[@]}" 2>/dev/null || true; \
106
+ wait "$${pids[@]}" 2>/dev/null || true; \
107
+ fi; \
108
+ }; \
109
+ trap cleanup EXIT INT TERM; \
110
+ for script in $(AVAILABLE_SERVER_SCRIPTS); do \
111
+ echo "[server] $$script"; \
112
+ $(RUBY_RUN) "$$script" & \
113
+ pids+=("$$!"); \
114
+ done; \
115
+ echo "All network servers are running. Press Ctrl-C to stop them."; \
116
+ wait
117
+
118
+ full-servers: check-dependencies
119
+ @$(MAKE) --no-print-directory servers
120
+
121
+ examples: dependency-report
122
+ @set -euo pipefail; \
123
+ for script in $(AVAILABLE_CLIENT_SCRIPTS); do \
124
+ echo; \
125
+ echo "[example] $$script"; \
126
+ $(RUBY_RUN) "$$script"; \
127
+ done
128
+
129
+ full-examples: check-dependencies
130
+ @$(MAKE) --no-print-directory examples
131
+
132
+ demo: check-base-dependencies dependency-report
133
+ @set -euo pipefail; \
134
+ pids=(); \
135
+ cleanup() { \
136
+ if (($${#pids[@]})); then \
137
+ kill "$${pids[@]}" 2>/dev/null || true; \
138
+ wait "$${pids[@]}" 2>/dev/null || true; \
139
+ fi; \
140
+ }; \
141
+ wait_for_port() { \
142
+ local host="$$1" port="$$2" attempt; \
143
+ for attempt in $$(seq 1 100); do \
144
+ if (exec 3<>"/dev/tcp/$$host/$$port") 2>/dev/null; then \
145
+ exec 3>&-; \
146
+ exec 3<&-; \
147
+ return 0; \
148
+ fi; \
149
+ sleep 0.1; \
150
+ done; \
151
+ echo "Timed out waiting for $$host:$$port" >&2; \
152
+ return 1; \
153
+ }; \
154
+ trap cleanup EXIT INT TERM; \
155
+ for script in $(AVAILABLE_SERVER_SCRIPTS); do \
156
+ echo "[server] $$script"; \
157
+ $(RUBY_RUN) "$$script" & \
158
+ pids+=("$$!"); \
159
+ done; \
160
+ for port in $(AVAILABLE_PORTS); do \
161
+ wait_for_port 127.0.0.1 "$$port"; \
162
+ done; \
163
+ sleep 0.2; \
164
+ $(MAKE) --no-print-directory examples
165
+
166
+ full-demo: check-dependencies
167
+ @$(MAKE) --no-print-directory demo
168
+
169
+ # Runs every pair that only needs Ruby's standard library (plus WEBrick).
170
+ standard-demo:
171
+ @$(MAKE) --no-print-directory demo DISABLE_OPTIONAL=1
172
+
173
+ grpc-python-setup:
174
+ @$(PYTHON) -m venv .venv-grpc
175
+ @.venv-grpc/bin/python -m pip install -r examples/servers/requirements-grpc.txt
176
+ @$(MAKE) --no-print-directory grpc-python-generate PYTHON_GRPC=.venv-grpc/bin/python
177
+
178
+ grpc-python-generate: check-python-grpc-tools
179
+ @mkdir -p examples/generated
180
+ @$(PYTHON_GRPC) -m grpc_tools.protoc \
181
+ -I proto \
182
+ --python_out=examples/generated \
183
+ --grpc_python_out=examples/generated \
184
+ proto/utcp.proto
185
+
186
+ grpc-python-server: check-python-grpc
187
+ @PORT="$(PYTHON_GRPC_PORT)" $(PYTHON_GRPC) examples/servers/grpc_server.py
188
+
189
+ grpc-python-client: check-python-grpc
190
+ @UTCP_GRPC_PORT="$(PYTHON_GRPC_PORT)" $(PYTHON_GRPC) examples/grpc_python.py
191
+
192
+ grpc-python-demo: check-python-grpc check-ruby-grpc
193
+ @set -euo pipefail; \
194
+ if (exec 3<>"/dev/tcp/127.0.0.1/$(PYTHON_GRPC_PORT)") 2>/dev/null; then \
195
+ exec 3>&-; exec 3<&-; \
196
+ echo "Port $(PYTHON_GRPC_PORT) is already in use." >&2; \
197
+ exit 1; \
198
+ fi; \
199
+ server_pid=""; \
200
+ cleanup() { \
201
+ if [[ -n "$$server_pid" ]]; then \
202
+ kill "$$server_pid" 2>/dev/null || true; \
203
+ wait "$$server_pid" 2>/dev/null || true; \
204
+ fi; \
205
+ }; \
206
+ trap cleanup EXIT INT TERM; \
207
+ PORT="$(PYTHON_GRPC_PORT)" $(PYTHON_GRPC) examples/servers/grpc_server.py & \
208
+ server_pid="$$!"; \
209
+ ready=0; \
210
+ for attempt in $$(seq 1 100); do \
211
+ if (exec 3<>"/dev/tcp/127.0.0.1/$(PYTHON_GRPC_PORT)") 2>/dev/null; then \
212
+ exec 3>&-; exec 3<&-; ready=1; break; \
213
+ fi; \
214
+ if ! kill -0 "$$server_pid" 2>/dev/null; then \
215
+ wait "$$server_pid"; exit 1; \
216
+ fi; \
217
+ sleep 0.1; \
218
+ done; \
219
+ if [[ "$$ready" != "1" ]]; then \
220
+ echo "Timed out waiting for Python gRPC server." >&2; exit 1; \
221
+ fi; \
222
+ UTCP_GRPC_PORT="$(PYTHON_GRPC_PORT)" $(RUBY_RUN) examples/grpc.rb; \
223
+ UTCP_GRPC_PORT="$(PYTHON_GRPC_PORT)" $(PYTHON_GRPC) examples/grpc_python.py
224
+
225
+ test:
226
+ $(RUBY_RUN) -S rake test
data/README.md ADDED
@@ -0,0 +1,331 @@
1
+ # ruby-utcp
2
+
3
+ `ruby-utcp` is a Ruby implementation of the Universal Tool Calling Protocol (UTCP) 1.1. It discovers tools from UTCP manuals, stores and searches them locally, and calls them directly over their native protocol.
4
+
5
+ The implementation follows the UTCP [v0.1 to v1.0 migration guide](https://www.utcp.io/migration-v0.1-to-v1.0) and [v1.0 to v1.1 migration guide](https://www.utcp.io/migration-v1.0-to-v1.1). It includes the v1 plugin architecture, structured data models, authentication, variable loading, protocol-specific errors, and the v1.1 secure-by-default protocol allow-list.
6
+
7
+ ## Installation
8
+
9
+ Add the gem to your bundle:
10
+
11
+ ```ruby
12
+ gem "ruby-utcp"
13
+ ```
14
+
15
+ Or build this checkout:
16
+
17
+ ```sh
18
+ gem build ruby-utcp.gemspec
19
+ gem install ./ruby-utcp-1.1.0.gem
20
+ ```
21
+
22
+ Ruby 2.6 or newer is supported for the core and 11 transports. The WebRTC default backend uses the optional `webrtc-ruby` gem, which requires Ruby 3.1 and `libdatachannel`; an application can instead inject its own peer adapter. The gRPC transport similarly loads the optional `grpc` gem only when used. There are no mandatory runtime dependencies outside Ruby's standard library.
23
+
24
+ ## Quick start
25
+
26
+ ```ruby
27
+ require "utcp"
28
+
29
+ client = UTCP::Client.create(
30
+ config: {
31
+ manual_call_templates: [
32
+ {
33
+ name: "weather_service",
34
+ call_template_type: "http",
35
+ url: "https://weather.example.com/utcp",
36
+ http_method: "GET"
37
+ }
38
+ ],
39
+ variables: {
40
+ WEATHER_API_KEY: ENV.fetch("WEATHER_API_KEY")
41
+ }
42
+ }
43
+ )
44
+
45
+ client.list_tools.each { |tool| puts tool.name }
46
+ result = client.call_tool("weather_service.get_weather", location: "Warsaw")
47
+ ```
48
+
49
+ `Client.create` registers the configured manuals before returning. Ruby calls are synchronous; `register_manual`, `search_tools`, and `call_tool` return their results directly.
50
+
51
+ ## UTCP 1.1 manuals
52
+
53
+ A manual describes tools and gives each tool its own call template:
54
+
55
+ ```json
56
+ {
57
+ "manual_version": "1.0.0",
58
+ "utcp_version": "1.1.0",
59
+ "info": {
60
+ "title": "Weather API",
61
+ "version": "1.0.0"
62
+ },
63
+ "tools": [
64
+ {
65
+ "name": "get_weather",
66
+ "description": "Get current weather",
67
+ "inputs": {
68
+ "type": "object",
69
+ "properties": {
70
+ "location": { "type": "string" }
71
+ },
72
+ "required": ["location"]
73
+ },
74
+ "outputs": { "type": "object" },
75
+ "tags": ["weather"],
76
+ "tool_call_template": {
77
+ "call_template_type": "http",
78
+ "url": "https://api.example.com/weather/{location}",
79
+ "http_method": "GET"
80
+ }
81
+ }
82
+ ]
83
+ }
84
+ ```
85
+
86
+ Registered names are qualified as `manual_name.tool_name`.
87
+
88
+ ## Secure-by-default protocol rules
89
+
90
+ UTCP 1.1 limits tools to the protocol used to discover their manual. An HTTP manual therefore registers HTTP tools by default and filters out CLI, text, and other tools.
91
+
92
+ Mixed-protocol manuals must opt in explicitly:
93
+
94
+ ```ruby
95
+ {
96
+ name: "mixed_tools",
97
+ call_template_type: "http",
98
+ url: "https://example.com/utcp",
99
+ allowed_communication_protocols: %w[http cli]
100
+ }
101
+ ```
102
+
103
+ The client checks this policy during both registration and every call. An omitted or empty allow-list means only the manual's own protocol is allowed.
104
+
105
+ ## All 12 transports
106
+
107
+ The gem registers all twelve transports present in the UTCP reference implementations. `file` remains available as an additional Ruby extension.
108
+
109
+ | Type | Call template | Behavior |
110
+ | --- | --- | --- |
111
+ | `http` | `HttpCallTemplate` | REST, OpenAPI discovery, redirects, auth |
112
+ | `sse` | `SseCallTemplate` | filtered Server-Sent Events and streaming enumeration |
113
+ | `streamable_http` | `StreamableHttpCallTemplate` | NDJSON, JSON Sequence, JSON, and binary chunks |
114
+ | `cli` | `CliCallTemplate` | safe argument interpolation and multi-step shell calls |
115
+ | `websocket` | `WebSocketCallTemplate` | RFC 6455 client, WSS security, persistent connections |
116
+ | `grpc` | `GrpcCallTemplate` | UTCP protobuf `GetManual`, `CallTool`, and server streaming |
117
+ | `graphql` | `GraphQLCallTemplate` | introspection, queries, mutations, and subscriptions |
118
+ | `tcp` | `TcpCallTemplate` | length-prefix, delimiter, fixed-size, and stream framing |
119
+ | `udp` | `UdpCallTemplate` | zero, one, or multiple response datagrams |
120
+ | `webrtc` | `WebRtcCallTemplate` | signaling plus request-correlated DataChannel messages |
121
+ | `mcp` | `McpCallTemplate` | MCP JSON-RPC over stdio or Streamable HTTP |
122
+ | `text` | `TextCallTemplate` | inline UTCP/OpenAPI documents and static text tools |
123
+
124
+ Complete runnable/configuration examples are in [examples/README.md](examples/README.md), with one file per transport.
125
+
126
+ Run `make` to start every available matching local server, execute its clients, and cleanly stop the servers. Missing optional gRPC/WebRTC backends are reported and skipped. `make full-demo` is the strict 12/12 target; `make standard-demo` always runs only the pairs that do not need native backends.
127
+
128
+ ### HTTP, SSE, and Streamable HTTP
129
+
130
+ HTTP templates support URL path parameters, query parameters, JSON or text bodies, input-to-header mapping, API keys, Basic auth, and OAuth2 client credentials.
131
+
132
+ ```ruby
133
+ {
134
+ call_template_type: "http",
135
+ url: "https://api.example.com/users/{user_id}",
136
+ http_method: "PATCH",
137
+ body_field: "body",
138
+ header_fields: ["X-Request-ID"],
139
+ auth: {
140
+ auth_type: "api_key",
141
+ api_key: "${API_TOKEN}",
142
+ var_name: "Authorization",
143
+ location: "header"
144
+ }
145
+ }
146
+ ```
147
+
148
+ Remote HTTP endpoints must use HTTPS. Plain HTTP is accepted only for loopback hosts, which keeps local development convenient. Redirect targets are checked again and credentials are removed on cross-origin redirects.
149
+
150
+ An HTTP, text, or file manual may also contain an OpenAPI 3 or Swagger 2 document. It is converted into UTCP tools automatically.
151
+
152
+ SSE and Streamable HTTP reuse the same URL, header, body, and auth conventions. Their streaming forms expose ordinary Ruby enumerators:
153
+
154
+ ```ruby
155
+ client.call_tool_streaming("events.watch", topic: "builds").each do |event|
156
+ puts event.inspect
157
+ end
158
+ ```
159
+
160
+ `streamable_http` is the v1 call-template discriminator. `http_stream` is also accepted as a compatibility alias for the Go reference implementation.
161
+
162
+ ### CLI
163
+
164
+ ```ruby
165
+ {
166
+ call_template_type: "cli",
167
+ commands: [
168
+ { command: "prepare-data", append_to_final_output: false },
169
+ { command: "weather UTCP_ARG_city_UTCP_END", append_to_final_output: true }
170
+ ],
171
+ env_vars: { "MODE" => "production" },
172
+ inherit_env_vars: ["PATH"],
173
+ working_dir: "/srv/tools"
174
+ }
175
+ ```
176
+
177
+ Commands run in one `/bin/sh` process, so working-directory changes and `$CMD_0_OUTPUT` references persist. Tool arguments are passed through dedicated environment variables after the shell parses the trusted command template, preventing argument values from injecting shell syntax. The child receives only a small default environment allow-list unless `inherit_env_vars` is set.
178
+
179
+ Only register CLI tools from manuals you trust: the command template itself is executable code.
180
+
181
+ ### WebSocket and GraphQL
182
+
183
+ WebSocket is implemented directly on Ruby sockets with RFC 6455 masking, fragmentation, ping/pong, TLS certificate verification, handshake verification, Basic/API-key/OAuth2 auth, and configurable JSON/text/binary responses. Plain `ws://` is restricted to literal loopback hosts.
184
+
185
+ GraphQL registration introspects query, mutation, and subscription roots. A discovered operation gets input/output schemas and a generated query; set `query`, `variable_types`, or `selection_set` on an explicit call template when the endpoint needs a custom selection:
186
+
187
+ ```ruby
188
+ {
189
+ call_template_type: "graphql",
190
+ url: "https://api.example.com/graphql",
191
+ operation_type: "query",
192
+ operation_name: "user",
193
+ query: "query User($id: ID!) { user(id: $id) { id name } }"
194
+ }
195
+ ```
196
+
197
+ Subscriptions use the `graphql-transport-ws` subprotocol through `call_tool_streaming`.
198
+
199
+ ### TCP and UDP
200
+
201
+ Socket templates can send JSON arguments or a text template containing `UTCP_ARG_name_UTCP_ARG`. TCP supports `length_prefix`, `delimiter`, `fixed_length`, and `stream` framing. UDP supports a configurable number of response datagrams, including zero for fire-and-forget calls. Timeouts are expressed in milliseconds in both templates.
202
+
203
+ ### gRPC
204
+
205
+ The gRPC transport interoperates with the reference `grpcpb.UTCPService`:
206
+
207
+ - `GetManual(Empty) returns (Manual)`
208
+ - `CallTool(ToolCallRequest) returns (ToolCallResponse)`
209
+ - `CallToolStream(ToolCallRequest) returns (stream ToolCallResponse)`
210
+
211
+ Add `gem "grpc"` to the consuming application. The protocol uses a tiny built-in protobuf codec for these UTCP messages, so generated Ruby classes are not required. A custom RPC adapter can be passed as `UTCP::GRPCProtocol.new(rpc_client_factory: ...)`.
212
+
213
+ The checked-in contract is [`proto/utcp.proto`](proto/utcp.proto). Its committed Python stubs are generated by `make grpc-python-generate` and shared by [`examples/servers/grpc_server.py`](examples/servers/grpc_server.py) and [`examples/grpc_python.py`](examples/grpc_python.py). Run both the Ruby and Python clients against the Python server with `make grpc-python-demo`.
214
+
215
+ ### MCP
216
+
217
+ MCP sessions implement initialization, notifications, `tools/list`, `tools/call`, optional `resources/list`/`resources/read`, session IDs, and both stdio and Streamable HTTP transports. With several servers, registered names use `manual.server.tool`:
218
+
219
+ ```ruby
220
+ {
221
+ name: "bridge",
222
+ call_template_type: "mcp",
223
+ config: {
224
+ mcpServers: {
225
+ local: { command: "ruby", args: ["server.rb"] },
226
+ remote: { transport: "http", url: "https://mcp.example.com/mcp" }
227
+ }
228
+ }
229
+ }
230
+ ```
231
+
232
+ ### WebRTC
233
+
234
+ WebRTC follows the reference signaling contract: `POST /connect` exchanges SDP and returns `sdp`, `candidates`, and `tools`; `POST /candidate` exchanges ICE candidates. DataChannel requests are JSON envelopes containing `id`, `tool`, and `args`, and responses correlate the same `id` with `result`.
235
+
236
+ The built-in peer uses `webrtc-ruby` and `libdatachannel`. For another native stack, pass `peer_factory:` to `UTCP::WebRTCProtocol`; the adapter contract is demonstrated by the protocol tests.
237
+
238
+ ### Text and file
239
+
240
+ Text templates parse a manual supplied directly in `content`. File templates read JSON or safe YAML relative to the client's `root_dir`.
241
+
242
+ ```ruby
243
+ UTCP::Client.create(config: {
244
+ manual_call_templates: [
245
+ { name: "local", call_template_type: "file", file_path: "tools.json" }
246
+ ]
247
+ })
248
+ ```
249
+
250
+ ## Variables and `.env` files
251
+
252
+ `${NAME}` and `$NAME` placeholders are resolved in this order:
253
+
254
+ 1. `config.variables`
255
+ 2. configured variable loaders
256
+ 3. the process environment
257
+
258
+ ```yaml
259
+ variables:
260
+ HOST: api.example.com
261
+ load_variables_from:
262
+ - variable_loader_type: dotenv
263
+ env_file_path: .env
264
+ ```
265
+
266
+ Manual-specific names are tried first. For a manual named `weather_api`, `${TOKEN}` first looks for `weather__api_TOKEN`, then `TOKEN`. Use `get_required_variables_for_manual_and_tools` or `get_required_variables_for_registered_tool` to inspect requirements without exposing values.
267
+
268
+ ## Migrating v0.1 documents
269
+
270
+ The helpers are non-destructive and return string-keyed hashes ready for `Client.create` or `Manual.from_h`:
271
+
272
+ ```ruby
273
+ config_1_1 = UTCP::Migration.config_v0_1_to_v1_1(old_config)
274
+ manual_1_1 = UTCP::Migration.manual_v0_1_to_v1_1(old_manual)
275
+
276
+ client = UTCP::Client.create(config: config_1_1)
277
+ manual = UTCP::Manual.from_h(manual_1_1)
278
+ ```
279
+
280
+ They rename `providers` to `manual_call_templates`, `provider_type` to `call_template_type`, `parameters` to `inputs`, `provider` to `tool_call_template`, and convert legacy CLI command arguments to `UTCP_ARG_name_UTCP_END` placeholders.
281
+
282
+ ## Search and repositories
283
+
284
+ The default thread-safe in-memory repository supports tool and manual lookup. Search ranks tag, name, and description matches:
285
+
286
+ ```ruby
287
+ client.search_tools("weather forecast", limit: 5, any_of_tags_required: ["weather"])
288
+ ```
289
+
290
+ Pass objects that implement the repository or search interfaces through `tool_repository` and `tool_search_strategy` to replace the defaults.
291
+
292
+ ## Custom protocol plugins
293
+
294
+ Register a call-template class and a protocol implementation before creating a client:
295
+
296
+ ```ruby
297
+ class QueueTemplate < UTCP::CallTemplate
298
+ attr_reader :queue
299
+
300
+ def initialize(queue:, call_template_type: "queue", **options)
301
+ super(call_template_type: call_template_type, **options)
302
+ @queue = queue
303
+ end
304
+
305
+ def to_h
306
+ super.merge("queue" => queue)
307
+ end
308
+ end
309
+
310
+ class QueueProtocol < UTCP::CommunicationProtocol
311
+ def register_manual(client, template)
312
+ # Return UTCP::RegisterManualResult
313
+ end
314
+
315
+ def deregister_manual(client, template); end
316
+
317
+ def call_tool(client, name, arguments, template)
318
+ # Invoke the queue's native API
319
+ end
320
+ end
321
+
322
+ UTCP.register_call_template("queue", QueueTemplate)
323
+ UTCP.register_protocol("queue", QueueProtocol.new)
324
+ ```
325
+
326
+ ## Development
327
+
328
+ ```sh
329
+ rake test
330
+ gem build ruby-utcp.gemspec
331
+ ```
data/examples/basic.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
+ require "utcp"
5
+
6
+ manual = {
7
+ manual_version: "1.0.0",
8
+ utcp_version: "1.1.0",
9
+ tools: [
10
+ {
11
+ name: "greeting",
12
+ description: "Return a greeting",
13
+ inputs: { type: "object" },
14
+ tool_call_template: {
15
+ call_template_type: "text",
16
+ content: "Hello from UTCP"
17
+ }
18
+ }
19
+ ]
20
+ }
21
+
22
+ client = UTCP::Client.create(config: {
23
+ manual_call_templates: [
24
+ {
25
+ name: "demo",
26
+ call_template_type: "text",
27
+ content: JSON.generate(manual)
28
+ }
29
+ ]
30
+ })
31
+
32
+ puts client.call_tool("demo.greeting")
33
+
data/examples/cli.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+ require "rbconfig"
6
+ require "utcp"
7
+
8
+ # A CLI manual is itself discovered by executing a command. The discovered tool also uses CLI.
9
+ manual = {
10
+ utcp_version: "1.1.0",
11
+ manual_version: "1.0.0",
12
+ tools: [{
13
+ name: "greet",
14
+ inputs: { type: "object", properties: { name: { type: "string" } }, required: ["name"] },
15
+ tool_call_template: {
16
+ call_template_type: "cli",
17
+ commands: [{ command: "printf 'Hello, %s!' UTCP_ARG_name_UTCP_END" }]
18
+ }
19
+ }]
20
+ }
21
+ encoded = Base64.strict_encode64(JSON.generate(manual))
22
+ discovery = "#{RbConfig.ruby} -rbase64 -e 'print Base64.decode64(ARGV.fetch(0))' #{encoded}"
23
+
24
+ client = UTCP::Client.create(config: {
25
+ manual_call_templates: [{
26
+ name: "shell",
27
+ call_template_type: "cli",
28
+ commands: [{ command: discovery }]
29
+ }]
30
+ })
31
+
32
+ puts client.call_tool("shell.greet", name: "Ruby")
@@ -0,0 +1 @@
1
+ """Python modules generated from proto/utcp.proto."""