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,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)
|
data/examples/graphql.rb
ADDED
|
@@ -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
|