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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
$stdout.sync = true
|
|
6
|
+
|
|
7
|
+
ARGF.each_line do |line|
|
|
8
|
+
message = JSON.parse(line)
|
|
9
|
+
next unless message.key?("id")
|
|
10
|
+
|
|
11
|
+
result = case message["method"]
|
|
12
|
+
when "initialize"
|
|
13
|
+
{
|
|
14
|
+
protocolVersion: message.dig("params", "protocolVersion") || "2025-06-18",
|
|
15
|
+
capabilities: { tools: {}, resources: {} },
|
|
16
|
+
serverInfo: { name: "ruby-example", version: "1.0.0" }
|
|
17
|
+
}
|
|
18
|
+
when "tools/list"
|
|
19
|
+
{
|
|
20
|
+
tools: [{
|
|
21
|
+
name: "echo",
|
|
22
|
+
description: "Echo a message",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: { message: { type: "string" } },
|
|
26
|
+
required: ["message"]
|
|
27
|
+
}
|
|
28
|
+
}]
|
|
29
|
+
}
|
|
30
|
+
when "tools/call"
|
|
31
|
+
text = message.dig("params", "arguments", "message").to_s
|
|
32
|
+
{ content: [{ type: "text", text: JSON.generate(echo: text) }] }
|
|
33
|
+
when "resources/list"
|
|
34
|
+
{ resources: [{ name: "welcome", uri: "memory://welcome", description: "Welcome text" }] }
|
|
35
|
+
when "resources/read"
|
|
36
|
+
{ contents: [{ uri: "memory://welcome", mimeType: "text/plain", text: "Hello from MCP" }] }
|
|
37
|
+
else
|
|
38
|
+
{}
|
|
39
|
+
end
|
|
40
|
+
puts JSON.generate(jsonrpc: "2.0", id: message["id"], result: result)
|
|
41
|
+
rescue JSON::ParserError => error
|
|
42
|
+
warn error.message
|
|
43
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "http_helpers"
|
|
4
|
+
|
|
5
|
+
port = Integer(ENV.fetch("PORT", "8081"))
|
|
6
|
+
base = "http://localhost:#{port}"
|
|
7
|
+
|
|
8
|
+
ExampleHTTP.server(port) do |server|
|
|
9
|
+
server.mount_proc("/utcp") do |_request, response|
|
|
10
|
+
ExampleHTTP.json(response, {
|
|
11
|
+
utcp_version: "1.1.0",
|
|
12
|
+
tools: [{
|
|
13
|
+
name: "watch",
|
|
14
|
+
tool_call_template: {
|
|
15
|
+
call_template_type: "sse",
|
|
16
|
+
url: "#{base}/watch",
|
|
17
|
+
event_type: "update"
|
|
18
|
+
}
|
|
19
|
+
}]
|
|
20
|
+
})
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
server.mount_proc("/watch") do |request, response|
|
|
24
|
+
topic = request.query["topic"] || "events"
|
|
25
|
+
response.status = 200
|
|
26
|
+
response["Content-Type"] = "text/event-stream"
|
|
27
|
+
response["Cache-Control"] = "no-cache"
|
|
28
|
+
response.chunked = true
|
|
29
|
+
response.body = proc do |output|
|
|
30
|
+
3.times do |index|
|
|
31
|
+
output.write("id: #{index + 1}\nevent: update\ndata: #{JSON.generate(topic: topic, number: index + 1)}\n\n")
|
|
32
|
+
sleep 0.2
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "http_helpers"
|
|
4
|
+
|
|
5
|
+
port = Integer(ENV.fetch("PORT", "8082"))
|
|
6
|
+
base = "http://localhost:#{port}"
|
|
7
|
+
|
|
8
|
+
ExampleHTTP.server(port) do |server|
|
|
9
|
+
server.mount_proc("/utcp") do |_request, response|
|
|
10
|
+
ExampleHTTP.json(response, {
|
|
11
|
+
utcp_version: "1.1.0",
|
|
12
|
+
tools: [{
|
|
13
|
+
name: "tokens",
|
|
14
|
+
tool_call_template: {
|
|
15
|
+
call_template_type: "streamable_http",
|
|
16
|
+
url: "#{base}/tokens",
|
|
17
|
+
http_method: "POST",
|
|
18
|
+
content_type: "application/json",
|
|
19
|
+
body_field: "body",
|
|
20
|
+
chunk_size: 32,
|
|
21
|
+
timeout: 60_000
|
|
22
|
+
}
|
|
23
|
+
}]
|
|
24
|
+
})
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
server.mount_proc("/tokens") do |request, response|
|
|
28
|
+
prompt = ExampleHTTP.request_json(request)["prompt"].to_s
|
|
29
|
+
response.status = 200
|
|
30
|
+
response["Content-Type"] = "application/x-ndjson"
|
|
31
|
+
response.chunked = true
|
|
32
|
+
response.body = proc do |output|
|
|
33
|
+
prompt.split.each_with_index do |token, index|
|
|
34
|
+
output.write(JSON.generate(index: index, token: token) + "\n")
|
|
35
|
+
sleep 0.15
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
port = Integer(ENV.fetch("PORT", "9000"))
|
|
7
|
+
|
|
8
|
+
def read_exact(socket, length)
|
|
9
|
+
value = +"".b
|
|
10
|
+
value << socket.readpartial(length - value.bytesize) while value.bytesize < length
|
|
11
|
+
value
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def read_message(socket)
|
|
15
|
+
length = read_exact(socket, 4).unpack1("N")
|
|
16
|
+
read_exact(socket, length)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def write_message(socket, value)
|
|
20
|
+
bytes = value.to_s.b
|
|
21
|
+
socket.write([bytes.bytesize].pack("N") + bytes)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
manual = {
|
|
25
|
+
utcp_version: "1.1.0",
|
|
26
|
+
tools: [{
|
|
27
|
+
name: "echo",
|
|
28
|
+
tool_call_template: {
|
|
29
|
+
call_template_type: "tcp",
|
|
30
|
+
host: "localhost",
|
|
31
|
+
port: port,
|
|
32
|
+
framing_strategy: "length_prefix",
|
|
33
|
+
length_prefix_bytes: 4,
|
|
34
|
+
length_prefix_endian: "big",
|
|
35
|
+
request_data_format: "json",
|
|
36
|
+
response_byte_format: "utf-8"
|
|
37
|
+
}
|
|
38
|
+
}]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
server = TCPServer.new("127.0.0.1", port)
|
|
42
|
+
warn "Listening on tcp://127.0.0.1:#{port}"
|
|
43
|
+
loop do
|
|
44
|
+
socket = server.accept
|
|
45
|
+
Thread.new(socket) do |client|
|
|
46
|
+
request = JSON.parse(read_message(client))
|
|
47
|
+
response = if request["type"] == "utcp"
|
|
48
|
+
JSON.generate(manual)
|
|
49
|
+
else
|
|
50
|
+
"Hello over TCP: #{request["message"]}"
|
|
51
|
+
end
|
|
52
|
+
write_message(client, response)
|
|
53
|
+
rescue EOFError, IOError, SystemCallError, JSON::ParserError
|
|
54
|
+
nil
|
|
55
|
+
ensure
|
|
56
|
+
client.close unless client.closed?
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
port = Integer(ENV.fetch("PORT", "9001"))
|
|
7
|
+
socket = UDPSocket.new
|
|
8
|
+
socket.bind("127.0.0.1", port)
|
|
9
|
+
warn "Listening on udp://127.0.0.1:#{port}"
|
|
10
|
+
|
|
11
|
+
manual = {
|
|
12
|
+
utcp_version: "1.1.0",
|
|
13
|
+
tools: [{
|
|
14
|
+
name: "echo",
|
|
15
|
+
tool_call_template: {
|
|
16
|
+
call_template_type: "udp",
|
|
17
|
+
host: "localhost",
|
|
18
|
+
port: port,
|
|
19
|
+
number_of_response_datagrams: 1,
|
|
20
|
+
request_data_format: "json",
|
|
21
|
+
response_byte_format: "utf-8"
|
|
22
|
+
}
|
|
23
|
+
}]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
loop do
|
|
27
|
+
bytes, sender = socket.recvfrom(65_535)
|
|
28
|
+
request = JSON.parse(bytes)
|
|
29
|
+
response = request["type"] == "utcp" ? JSON.generate(manual) : "Hello over UDP: #{request["message"]}"
|
|
30
|
+
socket.send(response, 0, sender[3], sender[1])
|
|
31
|
+
rescue JSON::ParserError => error
|
|
32
|
+
warn error.message
|
|
33
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "utcp"
|
|
5
|
+
gem "webrtc-ruby", ">= 1.0.0"
|
|
6
|
+
require "webrtc"
|
|
7
|
+
require_relative "http_helpers"
|
|
8
|
+
|
|
9
|
+
WebRTC.init
|
|
10
|
+
port = Integer(ENV.fetch("PORT", "8084"))
|
|
11
|
+
peers = {}
|
|
12
|
+
peers_mutex = Mutex.new
|
|
13
|
+
|
|
14
|
+
def wait_for_ice(peer, timeout = 5)
|
|
15
|
+
mutex = Mutex.new
|
|
16
|
+
condition = ConditionVariable.new
|
|
17
|
+
complete = peer.ice_gathering_state == :complete
|
|
18
|
+
peer.on_ice_gathering_state_change do
|
|
19
|
+
mutex.synchronize do
|
|
20
|
+
complete = peer.ice_gathering_state == :complete
|
|
21
|
+
condition.broadcast if complete
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
25
|
+
mutex.synchronize do
|
|
26
|
+
until complete
|
|
27
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
28
|
+
break unless remaining.positive?
|
|
29
|
+
condition.wait(mutex, remaining)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
ExampleHTTP.server(port) do |server|
|
|
35
|
+
server.mount_proc("/connect") do |request, response|
|
|
36
|
+
payload = ExampleHTTP.request_json(request)
|
|
37
|
+
candidates = []
|
|
38
|
+
peer = WebRTC::RTCPeerConnection.new
|
|
39
|
+
peer.on_ice_candidate { |candidate| candidates << candidate if candidate }
|
|
40
|
+
peer.on_data_channel do |channel|
|
|
41
|
+
channel.on_message do |event|
|
|
42
|
+
envelope = JSON.parse(event.data)
|
|
43
|
+
result = envelope.dig("args", "message") if envelope["tool"] == "echo"
|
|
44
|
+
channel.send_text(JSON.generate(id: envelope["id"], result: result))
|
|
45
|
+
rescue JSON::ParserError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
offer = WebRTC::RTCSessionDescription.new(type: :offer, sdp: payload.fetch("sdp"))
|
|
51
|
+
peer.set_remote_description(offer).await
|
|
52
|
+
answer = peer.create_answer.await
|
|
53
|
+
peer.set_local_description(answer).await
|
|
54
|
+
wait_for_ice(peer)
|
|
55
|
+
peers_mutex.synchronize { peers[payload.fetch("peer_id")] = peer }
|
|
56
|
+
ExampleHTTP.json(response, {
|
|
57
|
+
sdp: peer.local_description.sdp,
|
|
58
|
+
candidates: candidates.map(&:to_h),
|
|
59
|
+
tools: [{ name: "echo", description: "Echo over a WebRTC DataChannel" }]
|
|
60
|
+
})
|
|
61
|
+
rescue StandardError => error
|
|
62
|
+
ExampleHTTP.json(response, { error: error.message }, status: 500)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
server.mount_proc("/candidate") do |request, response|
|
|
66
|
+
payload = ExampleHTTP.request_json(request)
|
|
67
|
+
peer = peers_mutex.synchronize { peers[payload["peer_id"]] }
|
|
68
|
+
if peer
|
|
69
|
+
candidate = WebRTC::RTCIceCandidate.new(UTCP::Utils.symbolize_keys(payload.fetch("candidate")))
|
|
70
|
+
peer.add_ice_candidate(candidate).await
|
|
71
|
+
ExampleHTTP.json(response, { ok: true })
|
|
72
|
+
else
|
|
73
|
+
ExampleHTTP.json(response, { error: "unknown peer" }, status: 404)
|
|
74
|
+
end
|
|
75
|
+
rescue StandardError => error
|
|
76
|
+
ExampleHTTP.json(response, { error: error.message }, status: 400)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "digest/sha1"
|
|
5
|
+
require "json"
|
|
6
|
+
require "socket"
|
|
7
|
+
|
|
8
|
+
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
9
|
+
port = Integer(ENV.fetch("PORT", "8083"))
|
|
10
|
+
endpoint = "ws://localhost:#{port}/utcp"
|
|
11
|
+
|
|
12
|
+
def read_exact(socket, length)
|
|
13
|
+
value = +"".b
|
|
14
|
+
value << socket.readpartial(length - value.bytesize) while value.bytesize < length
|
|
15
|
+
value
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def read_frame(socket)
|
|
19
|
+
first, second = read_exact(socket, 2).unpack("CC")
|
|
20
|
+
opcode = first & 0x0F
|
|
21
|
+
length = second & 0x7F
|
|
22
|
+
length = read_exact(socket, 2).unpack1("n") if length == 126
|
|
23
|
+
length = read_exact(socket, 8).unpack1("Q>") if length == 127
|
|
24
|
+
mask = (second & 0x80).zero? ? nil : read_exact(socket, 4)
|
|
25
|
+
payload = read_exact(socket, length)
|
|
26
|
+
if mask
|
|
27
|
+
payload = payload.bytes.each_with_index.map { |byte, index| byte ^ mask.getbyte(index % 4) }.pack("C*")
|
|
28
|
+
end
|
|
29
|
+
[opcode, payload]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def write_frame(socket, payload, opcode = 0x1)
|
|
33
|
+
bytes = payload.to_s.b
|
|
34
|
+
header = [0x80 | opcode].pack("C")
|
|
35
|
+
header << if bytes.bytesize < 126
|
|
36
|
+
[bytes.bytesize].pack("C")
|
|
37
|
+
elsif bytes.bytesize <= 65_535
|
|
38
|
+
[126, bytes.bytesize].pack("Cn")
|
|
39
|
+
else
|
|
40
|
+
[127, bytes.bytesize].pack("CQ>")
|
|
41
|
+
end
|
|
42
|
+
socket.write(header + bytes)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
server = TCPServer.new("127.0.0.1", port)
|
|
46
|
+
warn "Listening on #{endpoint}"
|
|
47
|
+
|
|
48
|
+
loop do
|
|
49
|
+
socket = server.accept
|
|
50
|
+
Thread.new(socket) do |client|
|
|
51
|
+
header = +""
|
|
52
|
+
header << client.readpartial(1024) until header.include?("\r\n\r\n")
|
|
53
|
+
headers = header.split("\r\n").drop(1).each_with_object({}) do |line, values|
|
|
54
|
+
name, value = line.split(":", 2)
|
|
55
|
+
values[name.downcase] = value.to_s.strip
|
|
56
|
+
end
|
|
57
|
+
accept = Base64.strict_encode64(Digest::SHA1.digest(headers.fetch("sec-websocket-key") + GUID))
|
|
58
|
+
response = +"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n"
|
|
59
|
+
response << "Sec-WebSocket-Protocol: #{headers["sec-websocket-protocol"]}\r\n" if headers["sec-websocket-protocol"]
|
|
60
|
+
client.write(response + "\r\n")
|
|
61
|
+
|
|
62
|
+
loop do
|
|
63
|
+
opcode, bytes = read_frame(client)
|
|
64
|
+
break if opcode == 0x8
|
|
65
|
+
if opcode == 0x9
|
|
66
|
+
write_frame(client, bytes, 0xA)
|
|
67
|
+
next
|
|
68
|
+
end
|
|
69
|
+
message = JSON.parse(bytes)
|
|
70
|
+
if message["type"] == "utcp"
|
|
71
|
+
write_frame(client, JSON.generate(
|
|
72
|
+
utcp_version: "1.1.0",
|
|
73
|
+
tools: [{
|
|
74
|
+
name: "echo",
|
|
75
|
+
tool_call_template: {
|
|
76
|
+
call_template_type: "websocket",
|
|
77
|
+
url: endpoint,
|
|
78
|
+
protocol: "utcp-v1",
|
|
79
|
+
response_format: "json"
|
|
80
|
+
}
|
|
81
|
+
}]
|
|
82
|
+
))
|
|
83
|
+
else
|
|
84
|
+
write_frame(client, JSON.generate(echo: message["message"]))
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
rescue EOFError, IOError, SystemCallError, JSON::ParserError
|
|
88
|
+
nil
|
|
89
|
+
ensure
|
|
90
|
+
client.close unless client.closed?
|
|
91
|
+
end
|
|
92
|
+
end
|
data/examples/sse.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "utcp"
|
|
4
|
+
|
|
5
|
+
# The discovery URL returns JSON; the discovered tool template points to an SSE endpoint.
|
|
6
|
+
client = UTCP::Client.create(config: {
|
|
7
|
+
manual_call_templates: [{
|
|
8
|
+
name: "events",
|
|
9
|
+
call_template_type: "sse",
|
|
10
|
+
url: ENV.fetch("UTCP_SSE_MANUAL", "http://localhost:8081/utcp")
|
|
11
|
+
}]
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
client.call_tool_streaming("events.watch", topic: "builds") do |event|
|
|
15
|
+
puts "event: #{event.inspect}"
|
|
16
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "utcp"
|
|
4
|
+
|
|
5
|
+
# Supports NDJSON, JSON Sequence, JSON, and arbitrary binary chunks.
|
|
6
|
+
client = UTCP::Client.create(config: {
|
|
7
|
+
manual_call_templates: [{
|
|
8
|
+
name: "generator",
|
|
9
|
+
call_template_type: "streamable_http",
|
|
10
|
+
url: ENV.fetch("UTCP_STREAM_MANUAL", "http://localhost:8082/utcp"),
|
|
11
|
+
http_method: "GET",
|
|
12
|
+
chunk_size: 4096,
|
|
13
|
+
timeout: 60_000
|
|
14
|
+
}]
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
client.call_tool_streaming("generator.tokens", body: { prompt: "Hello from Ruby" }) { |chunk| puts chunk.inspect }
|
data/examples/tcp.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "utcp"
|
|
4
|
+
|
|
5
|
+
# The server answers the framed discovery message {"type":"utcp"} with a manual.
|
|
6
|
+
client = UTCP::Client.create(config: {
|
|
7
|
+
manual_call_templates: [{
|
|
8
|
+
name: "tcp_service",
|
|
9
|
+
call_template_type: "tcp",
|
|
10
|
+
host: ENV.fetch("UTCP_TCP_HOST", "localhost"),
|
|
11
|
+
port: Integer(ENV.fetch("UTCP_TCP_PORT", "9000")),
|
|
12
|
+
framing_strategy: "length_prefix",
|
|
13
|
+
length_prefix_bytes: 4,
|
|
14
|
+
length_prefix_endian: "big",
|
|
15
|
+
request_data_format: "json",
|
|
16
|
+
response_byte_format: "utf-8"
|
|
17
|
+
}]
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
puts client.call_tool("tcp_service.echo", message: "Hello over TCP")
|
data/examples/text.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "utcp"
|
|
5
|
+
|
|
6
|
+
manual = {
|
|
7
|
+
utcp_version: "1.1.0",
|
|
8
|
+
manual_version: "1.0.0",
|
|
9
|
+
tools: [{
|
|
10
|
+
name: "motd",
|
|
11
|
+
description: "Return a static message",
|
|
12
|
+
tool_call_template: {
|
|
13
|
+
call_template_type: "text",
|
|
14
|
+
content: "Hello from a text UTCP tool"
|
|
15
|
+
}
|
|
16
|
+
}]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
client = UTCP::Client.create(config: {
|
|
20
|
+
manual_call_templates: [{ name: "static", call_template_type: "text", content: JSON.generate(manual) }]
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
puts client.call_tool("static.motd")
|
data/examples/udp.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "utcp"
|
|
4
|
+
|
|
5
|
+
# The server answers {"type":"utcp"} with one manual datagram.
|
|
6
|
+
client = UTCP::Client.create(config: {
|
|
7
|
+
manual_call_templates: [{
|
|
8
|
+
name: "udp_service",
|
|
9
|
+
call_template_type: "udp",
|
|
10
|
+
host: ENV.fetch("UTCP_UDP_HOST", "localhost"),
|
|
11
|
+
port: Integer(ENV.fetch("UTCP_UDP_PORT", "9001")),
|
|
12
|
+
number_of_response_datagrams: 1,
|
|
13
|
+
request_data_format: "json",
|
|
14
|
+
response_byte_format: "utf-8"
|
|
15
|
+
}]
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
puts client.call_tool("udp_service.echo", message: "Hello over UDP")
|
data/examples/webrtc.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "utcp"
|
|
4
|
+
|
|
5
|
+
# Requires Ruby 3.1+, `gem "webrtc-ruby"`, and libdatachannel.
|
|
6
|
+
# POST /connect exchanges SDP and returns {sdp, candidates, tools}; /candidate accepts ICE.
|
|
7
|
+
client = UTCP::Client.create(config: {
|
|
8
|
+
manual_call_templates: [{
|
|
9
|
+
name: "rtc",
|
|
10
|
+
call_template_type: "webrtc",
|
|
11
|
+
signaling_server: ENV.fetch("UTCP_WEBRTC_SIGNALING", "http://localhost:8084"),
|
|
12
|
+
peer_id: ENV.fetch("UTCP_WEBRTC_PEER_ID", "ruby-client"),
|
|
13
|
+
data_channel_name: "utcp",
|
|
14
|
+
ice_servers: [{ urls: "stun:stun.l.google.com:19302" }]
|
|
15
|
+
}]
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
puts client.call_tool("rtc.echo", message: "Hello over WebRTC").inspect
|
|
19
|
+
client.close
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "utcp"
|
|
4
|
+
|
|
5
|
+
# The server first answers {"type":"utcp"} with a manual, then handles tool messages.
|
|
6
|
+
client = UTCP::Client.create(config: {
|
|
7
|
+
manual_call_templates: [{
|
|
8
|
+
name: "realtime",
|
|
9
|
+
call_template_type: "websocket",
|
|
10
|
+
url: ENV.fetch("UTCP_WEBSOCKET_URL", "ws://localhost:8083/utcp"),
|
|
11
|
+
protocol: "utcp-v1",
|
|
12
|
+
keep_alive: true,
|
|
13
|
+
response_format: "json"
|
|
14
|
+
}]
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
puts client.call_tool("realtime.echo", message: "Hello over WebSocket").inspect
|
data/lib/ruby-utcp.rb
ADDED