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
@@ -0,0 +1,46 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: utcp.proto
5
+ # Protobuf Python Version: 7.35.1
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 7,
15
+ 35,
16
+ 1,
17
+ '',
18
+ 'utcp.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+
26
+
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nutcp.proto\x12\x06grpcpb\"\x07\n\x05\x45mpty\")\n\x04Tool\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\"6\n\x06Manual\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x1b\n\x05tools\x18\x02 \x03(\x0b\x32\x0c.grpcpb.Tool\"2\n\x0fToolCallRequest\x12\x0c\n\x04tool\x18\x01 \x01(\t\x12\x11\n\targs_json\x18\x02 \x01(\t\"\'\n\x10ToolCallResponse\x12\x13\n\x0bresult_json\x18\x01 \x01(\t2\xbf\x01\n\x0bUTCPService\x12*\n\tGetManual\x12\r.grpcpb.Empty\x1a\x0e.grpcpb.Manual\x12=\n\x08\x43\x61llTool\x12\x17.grpcpb.ToolCallRequest\x1a\x18.grpcpb.ToolCallResponse\x12\x45\n\x0e\x43\x61llToolStream\x12\x17.grpcpb.ToolCallRequest\x1a\x18.grpcpb.ToolCallResponse0\x01\x62\x06proto3')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'utcp_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ DESCRIPTOR._loaded_options = None
34
+ _globals['_EMPTY']._serialized_start=22
35
+ _globals['_EMPTY']._serialized_end=29
36
+ _globals['_TOOL']._serialized_start=31
37
+ _globals['_TOOL']._serialized_end=72
38
+ _globals['_MANUAL']._serialized_start=74
39
+ _globals['_MANUAL']._serialized_end=128
40
+ _globals['_TOOLCALLREQUEST']._serialized_start=130
41
+ _globals['_TOOLCALLREQUEST']._serialized_end=180
42
+ _globals['_TOOLCALLRESPONSE']._serialized_start=182
43
+ _globals['_TOOLCALLRESPONSE']._serialized_end=221
44
+ _globals['_UTCPSERVICE']._serialized_start=224
45
+ _globals['_UTCPSERVICE']._serialized_end=415
46
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,183 @@
1
+ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2
+ """Client and server classes corresponding to protobuf-defined services."""
3
+ import grpc
4
+ import warnings
5
+
6
+ import utcp_pb2 as utcp__pb2
7
+
8
+ GRPC_GENERATED_VERSION = '1.83.1'
9
+ GRPC_VERSION = grpc.__version__
10
+ _version_not_supported = False
11
+
12
+ try:
13
+ from grpc._utilities import first_version_is_lower
14
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
15
+ except ImportError:
16
+ _version_not_supported = True
17
+
18
+ if _version_not_supported:
19
+ raise RuntimeError(
20
+ f'The grpc package installed is at version {GRPC_VERSION},'
21
+ + ' but the generated code in utcp_pb2_grpc.py depends on'
22
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
23
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
24
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
25
+ )
26
+
27
+
28
+ class UTCPServiceStub:
29
+ """Missing associated documentation comment in .proto file."""
30
+
31
+ def __init__(self, channel):
32
+ """Constructor.
33
+
34
+ Args:
35
+ channel: A grpc.Channel.
36
+ """
37
+ self.GetManual = channel.unary_unary(
38
+ '/grpcpb.UTCPService/GetManual',
39
+ request_serializer=utcp__pb2.Empty.SerializeToString,
40
+ response_deserializer=utcp__pb2.Manual.FromString,
41
+ _registered_method=True)
42
+ self.CallTool = channel.unary_unary(
43
+ '/grpcpb.UTCPService/CallTool',
44
+ request_serializer=utcp__pb2.ToolCallRequest.SerializeToString,
45
+ response_deserializer=utcp__pb2.ToolCallResponse.FromString,
46
+ _registered_method=True)
47
+ self.CallToolStream = channel.unary_stream(
48
+ '/grpcpb.UTCPService/CallToolStream',
49
+ request_serializer=utcp__pb2.ToolCallRequest.SerializeToString,
50
+ response_deserializer=utcp__pb2.ToolCallResponse.FromString,
51
+ _registered_method=True)
52
+
53
+
54
+ class UTCPServiceServicer:
55
+ """Missing associated documentation comment in .proto file."""
56
+
57
+ def GetManual(self, request, context):
58
+ """Missing associated documentation comment in .proto file."""
59
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
60
+ context.set_details('Method not implemented!')
61
+ raise NotImplementedError('Method not implemented!')
62
+
63
+ def CallTool(self, request, context):
64
+ """Missing associated documentation comment in .proto file."""
65
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
66
+ context.set_details('Method not implemented!')
67
+ raise NotImplementedError('Method not implemented!')
68
+
69
+ def CallToolStream(self, request, context):
70
+ """Missing associated documentation comment in .proto file."""
71
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
72
+ context.set_details('Method not implemented!')
73
+ raise NotImplementedError('Method not implemented!')
74
+
75
+
76
+ def add_UTCPServiceServicer_to_server(servicer, server):
77
+ rpc_method_handlers = {
78
+ 'GetManual': grpc.unary_unary_rpc_method_handler(
79
+ servicer.GetManual,
80
+ request_deserializer=utcp__pb2.Empty.FromString,
81
+ response_serializer=utcp__pb2.Manual.SerializeToString,
82
+ ),
83
+ 'CallTool': grpc.unary_unary_rpc_method_handler(
84
+ servicer.CallTool,
85
+ request_deserializer=utcp__pb2.ToolCallRequest.FromString,
86
+ response_serializer=utcp__pb2.ToolCallResponse.SerializeToString,
87
+ ),
88
+ 'CallToolStream': grpc.unary_stream_rpc_method_handler(
89
+ servicer.CallToolStream,
90
+ request_deserializer=utcp__pb2.ToolCallRequest.FromString,
91
+ response_serializer=utcp__pb2.ToolCallResponse.SerializeToString,
92
+ ),
93
+ }
94
+ generic_handler = grpc.method_handlers_generic_handler(
95
+ 'grpcpb.UTCPService', rpc_method_handlers)
96
+ server.add_generic_rpc_handlers((generic_handler,))
97
+ server.add_registered_method_handlers('grpcpb.UTCPService', rpc_method_handlers)
98
+
99
+
100
+ # This class is part of an EXPERIMENTAL API.
101
+ class UTCPService:
102
+ """Missing associated documentation comment in .proto file."""
103
+
104
+ @staticmethod
105
+ def GetManual(request,
106
+ target,
107
+ options=(),
108
+ channel_credentials=None,
109
+ call_credentials=None,
110
+ insecure=False,
111
+ compression=None,
112
+ wait_for_ready=None,
113
+ timeout=None,
114
+ metadata=None):
115
+ return grpc.experimental.unary_unary(
116
+ request,
117
+ target,
118
+ '/grpcpb.UTCPService/GetManual',
119
+ utcp__pb2.Empty.SerializeToString,
120
+ utcp__pb2.Manual.FromString,
121
+ options,
122
+ channel_credentials,
123
+ insecure,
124
+ call_credentials,
125
+ compression,
126
+ wait_for_ready,
127
+ timeout,
128
+ metadata,
129
+ _registered_method=True)
130
+
131
+ @staticmethod
132
+ def CallTool(request,
133
+ target,
134
+ options=(),
135
+ channel_credentials=None,
136
+ call_credentials=None,
137
+ insecure=False,
138
+ compression=None,
139
+ wait_for_ready=None,
140
+ timeout=None,
141
+ metadata=None):
142
+ return grpc.experimental.unary_unary(
143
+ request,
144
+ target,
145
+ '/grpcpb.UTCPService/CallTool',
146
+ utcp__pb2.ToolCallRequest.SerializeToString,
147
+ utcp__pb2.ToolCallResponse.FromString,
148
+ options,
149
+ channel_credentials,
150
+ insecure,
151
+ call_credentials,
152
+ compression,
153
+ wait_for_ready,
154
+ timeout,
155
+ metadata,
156
+ _registered_method=True)
157
+
158
+ @staticmethod
159
+ def CallToolStream(request,
160
+ target,
161
+ options=(),
162
+ channel_credentials=None,
163
+ call_credentials=None,
164
+ insecure=False,
165
+ compression=None,
166
+ wait_for_ready=None,
167
+ timeout=None,
168
+ metadata=None):
169
+ return grpc.experimental.unary_stream(
170
+ request,
171
+ target,
172
+ '/grpcpb.UTCPService/CallToolStream',
173
+ utcp__pb2.ToolCallRequest.SerializeToString,
174
+ utcp__pb2.ToolCallResponse.FromString,
175
+ options,
176
+ channel_credentials,
177
+ insecure,
178
+ call_credentials,
179
+ compression,
180
+ wait_for_ready,
181
+ timeout,
182
+ metadata,
183
+ _registered_method=True)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "utcp"
4
+
5
+ # Registration runs GraphQL introspection and turns root fields into UTCP tools.
6
+ client = UTCP::Client.create(config: {
7
+ manual_call_templates: [{
8
+ name: "local_graph",
9
+ call_template_type: "graphql",
10
+ url: ENV.fetch("UTCP_GRAPHQL_URL", "http://localhost:8085/graphql")
11
+ }]
12
+ })
13
+
14
+ puts client.list_tools.map(&:name)
15
+ puts client.call_tool("local_graph.hello", name: "Ruby").inspect
data/examples/grpc.rb ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ begin
6
+ require "grpc"
7
+ rescue LoadError => load_error
8
+ unless ENV["UTCP_RBENV_REEXEC"] == "1"
9
+ rbenv = [ENV["RBENV_BIN"], "/opt/homebrew/bin/rbenv", "/usr/local/bin/rbenv", "rbenv"]
10
+ .compact.find { |candidate| candidate == "rbenv" || File.executable?(candidate) }
11
+ begin
12
+ selected_ruby = IO.popen([rbenv, "which", "ruby"], err: File::NULL, &:read).to_s.strip
13
+ unless selected_ruby.empty? || File.expand_path(selected_ruby) == File.expand_path(RbConfig.ruby)
14
+ root = File.expand_path("..", __dir__)
15
+ exec({ "UTCP_RBENV_REEXEC" => "1" }, selected_ruby, "-I#{File.join(root, "lib")}", __FILE__, *ARGV)
16
+ end
17
+ rescue Errno::ENOENT
18
+ nil
19
+ end
20
+ end
21
+
22
+ warn "Unable to load grpc with #{RbConfig.ruby}: #{load_error.message}"
23
+ warn "Install it for the active Ruby with: gem install grpc"
24
+ exit 1
25
+ end
26
+
27
+ require "utcp"
28
+
29
+ # Add `gem "grpc"` to the application. The server implements grpcpb.UTCPService
30
+ # with GetManual, CallTool, and optionally CallToolStream.
31
+ client = UTCP::Client.create(config: {
32
+ manual_call_templates: [{
33
+ name: "rpc",
34
+ call_template_type: "grpc",
35
+ host: ENV.fetch("UTCP_GRPC_HOST", "localhost"),
36
+ port: Integer(ENV.fetch("UTCP_GRPC_PORT", "50051")),
37
+ service_name: "grpcpb.UTCPService",
38
+ use_ssl: ENV["UTCP_GRPC_TLS"] == "1"
39
+ }]
40
+ })
41
+
42
+ puts client.call_tool("rpc.echo", message: "Hello over gRPC").inspect
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python3
2
+ """Call the generated UTCP gRPC stub against the example server."""
3
+
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import sys
8
+
9
+ ROOT = Path(__file__).resolve().parents[1]
10
+ VENV_ROOT = ROOT / ".venv-grpc"
11
+ VENV_PYTHON = VENV_ROOT / "bin" / "python"
12
+ if VENV_PYTHON.exists() and Path(sys.prefix).resolve() != VENV_ROOT.resolve():
13
+ os.execv(str(VENV_PYTHON), [str(VENV_PYTHON), __file__, *sys.argv[1:]])
14
+
15
+ try:
16
+ import grpc
17
+ except ImportError as error:
18
+ raise SystemExit("Missing grpcio. Run: make grpc-python-setup") from error
19
+
20
+
21
+ GENERATED_DIR = Path(__file__).resolve().parent / "generated"
22
+ sys.path.insert(0, str(GENERATED_DIR))
23
+
24
+ try:
25
+ import utcp_pb2
26
+ import utcp_pb2_grpc
27
+ except (ImportError, RuntimeError) as error:
28
+ raise SystemExit(
29
+ "Missing or incompatible generated gRPC stubs. Run: make grpc-python-setup"
30
+ ) from error
31
+
32
+
33
+ def main():
34
+ host = os.environ.get("UTCP_GRPC_HOST", "127.0.0.1")
35
+ port = int(os.environ.get("UTCP_GRPC_PORT", "50051"))
36
+ with grpc.insecure_channel(f"{host}:{port}") as channel:
37
+ stub = utcp_pb2_grpc.UTCPServiceStub(channel)
38
+ manual = stub.GetManual(utcp_pb2.Empty(), timeout=5)
39
+ print("tools:", ", ".join(tool.name for tool in manual.tools))
40
+
41
+ response = stub.CallTool(
42
+ utcp_pb2.ToolCallRequest(
43
+ tool="echo",
44
+ args_json=json.dumps({"message": "Hello from Python gRPC client"}),
45
+ ),
46
+ timeout=5,
47
+ )
48
+ print(json.loads(response.result_json))
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()
data/examples/http.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "utcp"
4
+
5
+ # GET /utcp must return a UTCP 1.1 manual. Tool URLs are taken from that manual.
6
+ client = UTCP::Client.create(config: {
7
+ variables: { API_TOKEN: ENV.fetch("API_TOKEN", "development-token") },
8
+ manual_call_templates: [{
9
+ name: "rest",
10
+ call_template_type: "http",
11
+ url: ENV.fetch("UTCP_HTTP_MANUAL", "http://localhost:8080/utcp"),
12
+ http_method: "GET"
13
+ }]
14
+ })
15
+
16
+ puts client.list_tools.map(&:name)
17
+ puts client.call_tool("rest.echo", body: { message: "Hello over HTTP" }).inspect
data/examples/mcp.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "utcp"
4
+ require "rbconfig"
5
+
6
+ # Works with stdio and Streamable HTTP MCP servers. Tools are named server.tool.
7
+ server = if ENV["MCP_URL"]
8
+ { transport: "http", url: ENV.fetch("MCP_URL") }
9
+ else
10
+ {
11
+ transport: "stdio",
12
+ command: ENV.fetch("MCP_COMMAND", RbConfig.ruby),
13
+ args: ENV["MCP_COMMAND"] ? [] : [File.expand_path("servers/mcp_stdio_server.rb", __dir__)]
14
+ }
15
+ end
16
+
17
+ client = UTCP::Client.create(config: {
18
+ manual_call_templates: [{
19
+ name: "mcp_bridge",
20
+ call_template_type: "mcp",
21
+ config: { mcpServers: { demo: server } },
22
+ register_resources_as_tools: true
23
+ }]
24
+ })
25
+
26
+ puts client.list_tools.map(&:name)
27
+ puts client.call_tool("mcp_bridge.demo.echo", message: "Hello from MCP").inspect
28
+ client.close
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "http_helpers"
4
+
5
+ port = Integer(ENV.fetch("PORT", "8085"))
6
+
7
+ ExampleHTTP.server(port) do |server|
8
+ server.mount_proc("/graphql") do |request, response|
9
+ payload = ExampleHTTP.request_json(request)
10
+ if payload["query"].to_s.include?("UTCPIntrospection")
11
+ ExampleHTTP.json(response, {
12
+ data: {
13
+ __schema: {
14
+ queryType: {
15
+ name: "Query",
16
+ fields: [{
17
+ name: "hello",
18
+ description: "Return a greeting",
19
+ args: [{
20
+ name: "name",
21
+ description: "Person to greet",
22
+ defaultValue: nil,
23
+ type: { kind: "NON_NULL", name: nil, ofType: { kind: "SCALAR", name: "String" } }
24
+ }],
25
+ type: { kind: "SCALAR", name: "String" }
26
+ }]
27
+ },
28
+ mutationType: nil,
29
+ subscriptionType: nil,
30
+ types: [{ kind: "SCALAR", name: "String", fields: nil, enumValues: nil }]
31
+ }
32
+ }
33
+ })
34
+ else
35
+ name = payload.dig("variables", "name") || "world"
36
+ ExampleHTTP.json(response, { data: { hello: "Hello, #{name}!" } })
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python3
2
+ """Small UTCP gRPC server used by examples/grpc.rb."""
3
+
4
+ from concurrent import futures
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import signal
9
+ import sys
10
+
11
+ ROOT = Path(__file__).resolve().parents[2]
12
+ VENV_ROOT = ROOT / ".venv-grpc"
13
+ VENV_PYTHON = VENV_ROOT / "bin" / "python"
14
+ if VENV_PYTHON.exists() and Path(sys.prefix).resolve() != VENV_ROOT.resolve():
15
+ os.execv(str(VENV_PYTHON), [str(VENV_PYTHON), __file__, *sys.argv[1:]])
16
+
17
+ try:
18
+ import grpc
19
+ except ImportError as error:
20
+ raise SystemExit(
21
+ "Missing grpcio. Run: make grpc-python-setup"
22
+ ) from error
23
+
24
+ GENERATED_DIR = ROOT / "examples" / "generated"
25
+ sys.path.insert(0, str(GENERATED_DIR))
26
+
27
+ try:
28
+ import utcp_pb2
29
+ import utcp_pb2_grpc
30
+ except (ImportError, RuntimeError) as error:
31
+ raise SystemExit(
32
+ "Missing or incompatible generated gRPC stubs. Run: make grpc-python-setup"
33
+ ) from error
34
+
35
+
36
+ class UTCPService(utcp_pb2_grpc.UTCPServiceServicer):
37
+ def GetManual(self, _request, _context):
38
+ return utcp_pb2.Manual(
39
+ version="1.0.0",
40
+ tools=[
41
+ utcp_pb2.Tool(
42
+ name="echo",
43
+ description="Echo a JSON message from the Python gRPC server",
44
+ )
45
+ ],
46
+ )
47
+
48
+ def CallTool(self, request, context):
49
+ tool_name = request.tool.rsplit(".", 1)[-1]
50
+ if tool_name != "echo":
51
+ context.abort(grpc.StatusCode.NOT_FOUND, f"unknown tool: {request.tool}")
52
+
53
+ try:
54
+ arguments = json.loads(request.args_json or "{}")
55
+ except json.JSONDecodeError as error:
56
+ context.abort(grpc.StatusCode.INVALID_ARGUMENT, f"invalid args_json: {error}")
57
+
58
+ if not isinstance(arguments, dict):
59
+ context.abort(grpc.StatusCode.INVALID_ARGUMENT, "args_json must contain an object")
60
+
61
+ value = arguments.get("message", arguments.get("msg"))
62
+ return utcp_pb2.ToolCallResponse(result_json=json.dumps({"echo": value}))
63
+
64
+ def CallToolStream(self, request, context):
65
+ yield self.CallTool(request, context)
66
+
67
+
68
+ def main():
69
+ port = int(os.environ.get("PORT", "50051"))
70
+ address = f"127.0.0.1:{port}"
71
+ server = grpc.server(futures.ThreadPoolExecutor(max_workers=4))
72
+ utcp_pb2_grpc.add_UTCPServiceServicer_to_server(UTCPService(), server)
73
+ if server.add_insecure_port(address) == 0:
74
+ raise RuntimeError(f"unable to bind grpc://{address}")
75
+
76
+ server.start()
77
+ print(f"Listening on grpc://{address} (Python)", file=sys.stderr, flush=True)
78
+
79
+ stopping = False
80
+
81
+ def shutdown(_signal_number, _frame):
82
+ nonlocal stopping
83
+ if not stopping:
84
+ stopping = True
85
+ server.stop(grace=2)
86
+
87
+ signal.signal(signal.SIGINT, shutdown)
88
+ signal.signal(signal.SIGTERM, shutdown)
89
+
90
+ try:
91
+ server.wait_for_termination()
92
+ finally:
93
+ server.stop(grace=0)
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "grpc"
5
+ require "utcp"
6
+
7
+ class RawProtobufMessage
8
+ attr_reader :bytes
9
+
10
+ def initialize(bytes = "".b)
11
+ @bytes = bytes.to_s.b
12
+ end
13
+
14
+ def self.encode(message)
15
+ message.bytes
16
+ end
17
+
18
+ def self.decode(bytes)
19
+ new(bytes)
20
+ end
21
+ end
22
+
23
+ class ExampleUTCPService
24
+ include GRPC::GenericService
25
+ self.marshal_class_method = :encode
26
+ self.unmarshal_class_method = :decode
27
+ self.service_name = "grpcpb.UTCPService"
28
+
29
+ rpc :GetManual, RawProtobufMessage, RawProtobufMessage
30
+ rpc :CallTool, RawProtobufMessage, RawProtobufMessage
31
+ rpc :CallToolStream, RawProtobufMessage, stream(RawProtobufMessage)
32
+ end
33
+
34
+ class ExampleUTCPServer < ExampleUTCPService
35
+ def get_manual(_request, _call)
36
+ tool = UTCP::ProtobufWire.string_field(1, "echo") +
37
+ UTCP::ProtobufWire.string_field(2, "Echo a JSON message")
38
+ RawProtobufMessage.new(
39
+ UTCP::ProtobufWire.string_field(1, "1.0.0") +
40
+ UTCP::ProtobufWire.string_field(2, tool)
41
+ )
42
+ end
43
+
44
+ def call_tool(request, _call)
45
+ fields = UTCP::ProtobufWire.fields(request.bytes)
46
+ args = JSON.parse(fields[2].first.to_s)
47
+ result = JSON.generate(echo: args["message"])
48
+ RawProtobufMessage.new(UTCP::ProtobufWire.string_field(1, result))
49
+ end
50
+
51
+ def call_tool_stream(request, call)
52
+ response = call_tool(request, call)
53
+ [response].each
54
+ end
55
+ end
56
+
57
+ port = Integer(ENV.fetch("PORT", "50051"))
58
+ server = GRPC::RpcServer.new
59
+ server.add_http2_port("127.0.0.1:#{port}", :this_port_is_insecure)
60
+ server.handle(ExampleUTCPServer.new)
61
+ warn "Listening on grpc://127.0.0.1:#{port}"
62
+ server.run_till_terminated_or_interrupted(%w[INT TERM], 1)
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "webrick"
5
+
6
+ module ExampleHTTP
7
+ module_function
8
+
9
+ def server(port)
10
+ instance = WEBrick::HTTPServer.new(
11
+ BindAddress: "127.0.0.1",
12
+ Port: port,
13
+ AccessLog: [],
14
+ Logger: WEBrick::Log.new($stderr, WEBrick::Log::WARN)
15
+ )
16
+ trap("INT") { instance.shutdown }
17
+ trap("TERM") { instance.shutdown }
18
+ yield instance
19
+ warn "Listening on http://127.0.0.1:#{port}"
20
+ instance.start
21
+ end
22
+
23
+ def json(response, value, status: 200)
24
+ response.status = status
25
+ response["Content-Type"] = "application/json"
26
+ response.body = JSON.generate(value)
27
+ end
28
+
29
+ def request_json(request)
30
+ request.body.to_s.empty? ? {} : JSON.parse(request.body)
31
+ rescue JSON::ParserError
32
+ {}
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "http_helpers"
4
+
5
+ port = Integer(ENV.fetch("PORT", "8080"))
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
+ manual_version: "1.0.0",
13
+ tools: [{
14
+ name: "echo",
15
+ tool_call_template: {
16
+ call_template_type: "http",
17
+ url: "#{base}/echo",
18
+ http_method: "POST",
19
+ body_field: "body"
20
+ }
21
+ }]
22
+ })
23
+ end
24
+
25
+ server.mount_proc("/echo") do |request, response|
26
+ ExampleHTTP.json(response, ExampleHTTP.request_json(request))
27
+ end
28
+ end