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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f149ed8c39f7f9204dedf270af330dfca4a2a81af20da405e96c04b9b5102968
4
- data.tar.gz: 4ba6c0e77734d204553d4d5de568870e1cdcab6e95057d185e1f3b653137fda1
3
+ metadata.gz: 7152973bd68d5f99cedef389ef3511ef2e0184ae1d57ec6c1d50fe69cb6b0862
4
+ data.tar.gz: 165379f2822e471133fa9fc6adde1ffb84c56b0e3b07c28afbf78d5e87aec92a
5
5
  SHA512:
6
- metadata.gz: fc94f91b7227c7fef8d3a38532ead92eeb8360bcbc5ae8216c31e072f4e5de389121d93ffd1af284cfdcd81a166de159279e5b8d5d2a1739d465365a9c5883ea
7
- data.tar.gz: 4e6b711fb478658fe17ed4acbe9148b2f25b60e901e3424561a5e55d9488117dbc9e69a4772d7f58de2c241af268a43565bcd701a6591afc1df04af7acad3fdb
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
- exec.call_tool(t, arguments)
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
- res = http.request(req)
83
- # try to parse as JSON; fall back to raw string
84
- begin
85
- JSON.parse(res.body)
86
- rescue
87
- res.body
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(uri.host, uri.port, use_ssl: uri.scheme == "https")
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
- res.read_body do |chunk|
42
- yield chunk if block_given?
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
- nil
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.active?
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
- headers = [
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
- ].join("\r\n")
110
- sock.write(headers)
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.0
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-12 00:00:00.000000000 Z
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
- ```