rb-utcp 0.1.0 → 0.1.1
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 +4 -4
- data/lib/utcp/client.rb +6 -1
- data/lib/utcp/providers/http_provider.rb +15 -6
- data/lib/utcp/providers/http_stream_provider.rb +23 -5
- data/lib/utcp/providers/websocket_provider.rb +13 -6
- metadata +2 -3
- data/README.md +0 -129
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7152973bd68d5f99cedef389ef3511ef2e0184ae1d57ec6c1d50fe69cb6b0862
|
|
4
|
+
data.tar.gz: 165379f2822e471133fa9fc6adde1ffb84c56b0e3b07c28afbf78d5e87aec92a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d27bc315ede501296dace44f2277e2cf2fdb32089ff56d201a56d826432f7ded5e084ee33e50b3e0e2b21fb23745313d2593a3649542bf8ce44a0b14cf2e31c7
|
|
7
|
+
data.tar.gz: 7a183a6e766d2d19f58a71dd31c9d9767fe65198cafa3724e81314ddc97a08e245d51cc54745f9e9da97df9df579690415d6f749332b23a0ec1284cf60af1711
|
data/lib/utcp/client.rb
CHANGED
|
@@ -98,7 +98,12 @@ module Utcp
|
|
|
98
98
|
auth: auth,
|
|
99
99
|
body_field: p["body_field"]
|
|
100
100
|
)
|
|
101
|
-
|
|
101
|
+
if stream
|
|
102
|
+
raise ConfigError, "Streaming requires a block for HTTP" unless block_given?
|
|
103
|
+
exec.call_tool(t, arguments, &block)
|
|
104
|
+
else
|
|
105
|
+
exec.call_tool(t, arguments)
|
|
106
|
+
end
|
|
102
107
|
|
|
103
108
|
when "sse"
|
|
104
109
|
raise ConfigError, "Streaming requires a block for SSE" if stream && !block_given?
|
|
@@ -79,12 +79,21 @@ module Utcp
|
|
|
79
79
|
|
|
80
80
|
http = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https")
|
|
81
81
|
begin
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
if block_given?
|
|
83
|
+
http.request(req) do |res|
|
|
84
|
+
res.read_body do |chunk|
|
|
85
|
+
yield chunk
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
nil
|
|
89
|
+
else
|
|
90
|
+
res = http.request(req)
|
|
91
|
+
# try to parse as JSON; fall back to raw string
|
|
92
|
+
begin
|
|
93
|
+
JSON.parse(res.body)
|
|
94
|
+
rescue
|
|
95
|
+
res.body
|
|
96
|
+
end
|
|
88
97
|
end
|
|
89
98
|
ensure
|
|
90
99
|
http.finish if http.active?
|
|
@@ -35,18 +35,36 @@ module Utcp
|
|
|
35
35
|
@auth&.apply_headers(headers)
|
|
36
36
|
headers.each { |k, v| req[k] = v }
|
|
37
37
|
|
|
38
|
-
http = Net::HTTP.start(
|
|
38
|
+
http = Net::HTTP.start(
|
|
39
|
+
uri.host, uri.port,
|
|
40
|
+
use_ssl: uri.scheme == "https"
|
|
41
|
+
)
|
|
42
|
+
http.read_timeout = nil
|
|
43
|
+
http.open_timeout = 5
|
|
44
|
+
|
|
39
45
|
begin
|
|
40
46
|
http.request(req) do |res|
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
begin
|
|
48
|
+
res.read_body do |chunk|
|
|
49
|
+
yield chunk if block_given?
|
|
50
|
+
end
|
|
51
|
+
rescue EOFError
|
|
52
|
+
warn "[HttpStreamProvider] EOFError during chunk read — treating as normal stream end"
|
|
53
|
+
rescue IOError
|
|
54
|
+
warn "[HttpStreamProvider] IOError during chunk read — treating as normal stream end"
|
|
43
55
|
end
|
|
44
56
|
end
|
|
45
|
-
|
|
57
|
+
rescue EOFError
|
|
58
|
+
warn "[HttpStreamProvider] EOFError before body read — treating as end of stream"
|
|
59
|
+
rescue IOError
|
|
60
|
+
warn "[HttpStreamProvider] IOError before body read — treating as end of stream"
|
|
46
61
|
ensure
|
|
47
|
-
http.finish if http
|
|
62
|
+
http.finish if http&.active?
|
|
48
63
|
end
|
|
64
|
+
|
|
65
|
+
nil
|
|
49
66
|
end
|
|
67
|
+
|
|
50
68
|
end
|
|
51
69
|
end
|
|
52
70
|
end
|
|
@@ -32,9 +32,13 @@ module Utcp
|
|
|
32
32
|
uri = URI(Utils::Subst.apply(p["url"]))
|
|
33
33
|
raise ConfigError, "WebSocket requires ws:// or wss:// URL" unless %w[ws wss].include?(uri.scheme)
|
|
34
34
|
|
|
35
|
+
headers = Utils::Subst.apply(p["headers"] || {}).transform_keys(&:to_s)
|
|
36
|
+
@auth&.apply_query(uri) if @auth&.respond_to?(:apply_query)
|
|
37
|
+
@auth&.apply_headers(headers)
|
|
38
|
+
|
|
35
39
|
sock = connect_socket(uri)
|
|
36
40
|
begin
|
|
37
|
-
handshake(sock, uri)
|
|
41
|
+
handshake(sock, uri, headers)
|
|
38
42
|
# Compose a text message to send
|
|
39
43
|
payload = compose_payload(p, arguments)
|
|
40
44
|
if payload
|
|
@@ -94,20 +98,23 @@ module Utcp
|
|
|
94
98
|
end
|
|
95
99
|
end
|
|
96
100
|
|
|
97
|
-
def handshake(sock, uri)
|
|
101
|
+
def handshake(sock, uri, extra_headers = {})
|
|
98
102
|
key = Base64.strict_encode64(Random.new.bytes(16))
|
|
99
103
|
path = uri.request_uri
|
|
100
104
|
host = uri.host
|
|
101
|
-
|
|
105
|
+
host += ":#{uri.port}" if uri.port && uri.port != (uri.scheme == "wss" ? 443 : 80)
|
|
106
|
+
header_lines = [
|
|
102
107
|
"GET #{path} HTTP/1.1",
|
|
103
108
|
"Host: #{host}",
|
|
104
109
|
"Upgrade: websocket",
|
|
105
110
|
"Connection: Upgrade",
|
|
106
111
|
"Sec-WebSocket-Key: #{key}",
|
|
107
112
|
"Sec-WebSocket-Version: 13",
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
]
|
|
114
|
+
extra_headers.each { |k, v| header_lines << "#{k}: #{v}" }
|
|
115
|
+
header_lines << "" << ""
|
|
116
|
+
sock.write(header_lines.join("\r\n"))
|
|
117
|
+
sock.flush if sock.respond_to?(:flush)
|
|
111
118
|
|
|
112
119
|
status_line = sock.gets("\r\n") || ""
|
|
113
120
|
unless status_line.start_with?("HTTP/1.1 101")
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rb-utcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- UTCP contributors
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-08-
|
|
11
|
+
date: 2025-08-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Minimal Ruby implementation of UTCP with HTTP/SSE/HTTP-stream transports.
|
|
14
14
|
email:
|
|
@@ -19,7 +19,6 @@ extensions: []
|
|
|
19
19
|
extra_rdoc_files: []
|
|
20
20
|
files:
|
|
21
21
|
- LICENSE
|
|
22
|
-
- README.md
|
|
23
22
|
- bin/utcp
|
|
24
23
|
- lib/utcp.rb
|
|
25
24
|
- lib/utcp/auth.rb
|
data/README.md
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
# ruby-utcp (alpha)
|
|
2
|
-
|
|
3
|
-
A small, dependency-light Ruby implementation of the **Universal Tool Calling Protocol (UTCP)**.
|
|
4
|
-
It mirrors the core models — **Manual**, **Tool**, **Providers**, and **Auth** — and lets you
|
|
5
|
-
discover tools and call them over HTTP, SSE, and HTTP chunked streams.
|
|
6
|
-
|
|
7
|
-
> Status: early alpha, but usable for simple demos. Standard library only.
|
|
8
|
-
|
|
9
|
-
## Features
|
|
10
|
-
- Load one or more "manual providers" from `providers.json` (HTTP or local file).
|
|
11
|
-
- Store discovered tools in an in-memory repository.
|
|
12
|
-
- Call tools via HTTP (`GET/POST/PUT/PATCH/DELETE`), SSE, or HTTP chunked streaming.
|
|
13
|
-
- API Key, Basic, and OAuth2 Client Credentials auth (token cached in memory).
|
|
14
|
-
- Simple variable substitution for `${VAR}` using values from environment and `.env` files.
|
|
15
|
-
- Tiny search helper scoring tags + description to find relevant tools.
|
|
16
|
-
|
|
17
|
-
## Install
|
|
18
|
-
This is a vanilla Ruby project. No external gems are required.
|
|
19
|
-
```bash
|
|
20
|
-
ruby -v # Ruby 3.x recommended
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Quickstart
|
|
24
|
-
```bash
|
|
25
|
-
# 1) Unzip, cd in
|
|
26
|
-
cd ruby-utcp
|
|
27
|
-
|
|
28
|
-
# 2) (Optional) create a .env file with secrets
|
|
29
|
-
echo 'OPEN_WEATHER_API_KEY=replace-me' > .env
|
|
30
|
-
|
|
31
|
-
# 3) Run the example (uses httpbin.org)
|
|
32
|
-
ruby examples/basic_call.rb
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Layout
|
|
36
|
-
```
|
|
37
|
-
lib/utcp.rb
|
|
38
|
-
lib/utcp/version.rb
|
|
39
|
-
lib/utcp/client.rb
|
|
40
|
-
lib/utcp/tool.rb
|
|
41
|
-
lib/utcp/errors.rb
|
|
42
|
-
lib/utcp/utils/env_loader.rb
|
|
43
|
-
lib/utcp/utils/subst.rb
|
|
44
|
-
lib/utcp/auth.rb
|
|
45
|
-
lib/utcp/tool_repository.rb
|
|
46
|
-
lib/utcp/search.rb
|
|
47
|
-
lib/utcp/providers/base_provider.rb
|
|
48
|
-
lib/utcp/providers/http_provider.rb
|
|
49
|
-
lib/utcp/providers/sse_provider.rb
|
|
50
|
-
lib/utcp/providers/http_stream_provider.rb
|
|
51
|
-
bin/utcp
|
|
52
|
-
examples/providers.json
|
|
53
|
-
examples/tools_weather.json
|
|
54
|
-
examples/basic_call.rb
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## Example manual (local file)
|
|
58
|
-
See `examples/tools_weather.json` for a minimal UTCP manual that exposes two tools:
|
|
59
|
-
- `echo` (POST JSON to httpbin.org)
|
|
60
|
-
- `stream_http` (stream 20 JSON lines from httpbin.org)
|
|
61
|
-
|
|
62
|
-
## CLI
|
|
63
|
-
```bash
|
|
64
|
-
# List all discovered tools
|
|
65
|
-
ruby bin/utcp list examples/providers.json
|
|
66
|
-
|
|
67
|
-
# Call a tool (args as JSON)
|
|
68
|
-
ruby bin/utcp call examples/providers.json echo --args '{"message":"hello"}'
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## License
|
|
72
|
-
MPL-2.0
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
## New transports (alpha)
|
|
76
|
-
- **WebSocket**: minimal RFC6455 text-only client; great for echo/testing.
|
|
77
|
-
- **GraphQL**: POST query + variables to any GraphQL endpoint.
|
|
78
|
-
- **TCP/UDP**: raw sockets with simple `${var}` templating; includes local echo servers under `examples/dev/`.
|
|
79
|
-
- **CLI**: call local commands (use carefully!).
|
|
80
|
-
|
|
81
|
-
### Try them
|
|
82
|
-
Start local echo servers (optional, for TCP/UDP):
|
|
83
|
-
```bash
|
|
84
|
-
ruby examples/dev/echo_tcp_server.rb 5001
|
|
85
|
-
ruby examples/dev/echo_udp_server.rb 5002
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Use the extra providers file:
|
|
89
|
-
```bash
|
|
90
|
-
ruby bin/utcp list examples/providers_extra.json
|
|
91
|
-
ruby bin/utcp call examples/providers_extra.json ws_demo.ws_echo --args '{"text":"hello ws"}' --stream
|
|
92
|
-
ruby bin/utcp call examples/providers_extra.json cli_demo.shell_echo --args '{"msg":"hi from shell"}'
|
|
93
|
-
ruby bin/utcp call examples/providers_extra.json sock_demo.tcp_echo --args '{"name":"kamil"}'
|
|
94
|
-
ruby bin/utcp call examples/providers_extra.json gql_demo.country_by_code --args '{"code":"DE"}'
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
## MCP provider
|
|
99
|
-
This adds a minimal HTTP-based MCP bridge.
|
|
100
|
-
|
|
101
|
-
### Discovery
|
|
102
|
-
Manual discovery expects the server to return a UTCP manual at `{url}/manual` (configurable via `discovery_path`). Point a manual provider to `"provider_type": "mcp"` in `providers.json` to fetch tools.
|
|
103
|
-
|
|
104
|
-
### Calls
|
|
105
|
-
Tools with `"provider_type": "mcp"` will POST to `{url}/call` with:
|
|
106
|
-
```json
|
|
107
|
-
{ "tool": "<name>", "arguments": { "...": "..." } }
|
|
108
|
-
```
|
|
109
|
-
If the response is `text/event-stream` we parse SSE and yield each `data:` line; otherwise we stream raw chunks when `--stream` is used.
|
|
110
|
-
|
|
111
|
-
### Example
|
|
112
|
-
```bash
|
|
113
|
-
# assuming an MCP test server on http://localhost:8220/mcp
|
|
114
|
-
ruby bin/utcp list examples/providers_mcp.json
|
|
115
|
-
ruby bin/utcp call examples/providers_mcp.json mcp_demo.hello --args '{"name":"Kamil"}'
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
## Tests
|
|
120
|
-
This project uses **Minitest** (stdlib only).
|
|
121
|
-
|
|
122
|
-
Run all tests:
|
|
123
|
-
```bash
|
|
124
|
-
ruby bin/test
|
|
125
|
-
```
|
|
126
|
-
or
|
|
127
|
-
```bash
|
|
128
|
-
ruby -Ilib -Itest -rminitest/autorun test/*_test.rb
|
|
129
|
-
```
|