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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE +22 -0
- data/Makefile +226 -0
- data/README.md +331 -0
- data/examples/basic.rb +33 -0
- data/examples/cli.rb +32 -0
- data/examples/generated/__init__.py +1 -0
- data/examples/generated/utcp_pb2.py +46 -0
- data/examples/generated/utcp_pb2_grpc.py +183 -0
- data/examples/graphql.rb +15 -0
- data/examples/grpc.rb +42 -0
- data/examples/grpc_python.py +52 -0
- data/examples/http.rb +17 -0
- data/examples/mcp.rb +28 -0
- data/examples/servers/graphql_server.rb +39 -0
- data/examples/servers/grpc_server.py +97 -0
- data/examples/servers/grpc_server.rb +62 -0
- data/examples/servers/http_helpers.rb +34 -0
- data/examples/servers/http_server.rb +28 -0
- data/examples/servers/mcp_stdio_server.rb +43 -0
- data/examples/servers/requirements-grpc.txt +2 -0
- data/examples/servers/sse_server.rb +36 -0
- data/examples/servers/streamable_http_server.rb +39 -0
- data/examples/servers/tcp_server.rb +58 -0
- data/examples/servers/udp_server.rb +33 -0
- data/examples/servers/webrtc_server.rb +78 -0
- data/examples/servers/websocket_server.rb +92 -0
- data/examples/sse.rb +16 -0
- data/examples/streamable_http.rb +17 -0
- data/examples/tcp.rb +20 -0
- data/examples/text.rb +23 -0
- data/examples/udp.rb +18 -0
- data/examples/webrtc.rb +19 -0
- data/examples/websocket.rb +17 -0
- data/lib/ruby-utcp.rb +4 -0
- data/lib/utcp/client.rb +217 -0
- data/lib/utcp/config.rb +79 -0
- data/lib/utcp/errors.rb +48 -0
- data/lib/utcp/migration.rb +88 -0
- data/lib/utcp/models.rb +794 -0
- data/lib/utcp/openapi_converter.rb +179 -0
- data/lib/utcp/protocols/base.rb +97 -0
- data/lib/utcp/protocols/cli.rb +186 -0
- data/lib/utcp/protocols/file.rb +52 -0
- data/lib/utcp/protocols/graphql.rb +277 -0
- data/lib/utcp/protocols/grpc.rb +207 -0
- data/lib/utcp/protocols/http.rb +340 -0
- data/lib/utcp/protocols/http_stream_support.rb +122 -0
- data/lib/utcp/protocols/mcp.rb +339 -0
- data/lib/utcp/protocols/socket_support.rb +51 -0
- data/lib/utcp/protocols/sse.rb +107 -0
- data/lib/utcp/protocols/streamable_http.rb +78 -0
- data/lib/utcp/protocols/tcp.rb +143 -0
- data/lib/utcp/protocols/text.rb +44 -0
- data/lib/utcp/protocols/udp.rb +61 -0
- data/lib/utcp/protocols/webrtc.rb +217 -0
- data/lib/utcp/protocols/websocket.rb +350 -0
- data/lib/utcp/registry.rb +67 -0
- data/lib/utcp/repository.rb +137 -0
- data/lib/utcp/serializer.rb +71 -0
- data/lib/utcp/utils.rb +118 -0
- data/lib/utcp/variables.rb +170 -0
- data/lib/utcp/version.rb +6 -0
- data/lib/utcp.rb +70 -0
- data/proto/utcp.proto +31 -0
- 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."""
|