-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdata.js
More file actions
430 lines (425 loc) · 36.1 KB
/
Copy pathdata.js
File metadata and controls
430 lines (425 loc) · 36.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
export const REPOSITORY = 'https://github.com/universal-tool-calling-protocol/ruby-utcp';
export const quickstart = [
'require "utcp"',
'',
'# Connect to your tool manual',
'client = UTCP::Client.create(config: {',
' manual_call_templates: [{',
' name: "weather",',
' call_template_type: "http",',
' url: "https://api.example.com/utcp"',
' }]',
'})',
'',
'# Call a discovered tool',
'result = client.call_tool(',
' "weather.get_weather", location: "Warsaw"',
')',
'puts result',
'client.close',
].join('\n');
const transportExample = ({ type, name, tool, description, properties, args, template, streaming = false }) => {
const manual = JSON.stringify({
utcp_version: '1.1.0',
manual_version: '1.0.0',
tools: [{
name: tool,
description,
inputs: { type: 'object', properties, required: Object.keys(properties) },
tool_call_template: { call_template_type: type, ...template },
}],
}, null, 2);
const filename = type + '.manual.json';
const setup = (clientClass) => [
'require "utcp"',
'',
'# Save the Manual example as ' + filename,
'client = UTCP::' + clientClass + '.create(config: {',
' manual_call_templates: [{',
' name: "' + name + '",',
' call_template_type: "' + (type === 'text' ? 'text' : 'file') + '",',
type === 'text'
? ' content: File.read("' + filename + '"),'
: ' file_path: "' + filename + '",',
' allowed_communication_protocols: ["' + type + '"]',
' }]',
'})',
'',
'begin',
];
const argumentsList = ['"' + name + '.' + tool + '"', ...Object.entries(args).map(([key, value]) => key + ': ' + JSON.stringify(value))];
const callArguments = (indent) => argumentsList.map((argument, index) => indent + argument + (index < argumentsList.length - 1 ? ',' : ''));
const cleanup = ['ensure', ' client.close', 'end'];
return {
manual,
code: [
...setup('Client'),
' ' + (streaming ? 'client.call_tool_streaming(' : 'result = client.call_tool('),
...callArguments(' '),
streaming ? ' ).each do |item|' : ' )',
...(streaming ? [' puts item.inspect', ' end'] : [' puts result.inspect']),
...cleanup,
].join('\n'),
codeMode: [
...setup('CodeModeUtcpClient'),
' execution = client.call_tool_chain(<<~\'RUBY\', timeout: 10)',
' codemode.' + (streaming ? 'call_tool_stream(' : 'call_tool('),
...callArguments(' '),
' )',
' RUBY',
' puts execution["result"].inspect',
...cleanup,
].join('\n'),
};
};
export const protocols = [
{
id: 'http', name: 'HTTP', icon: 'globe', category: 'The everyday essential',
title: 'Your APIs, connected.',
description: 'Turn existing REST endpoints into discoverable tools. Load a UTCP manual or an OpenAPI document, then call your API directly.',
tags: ['REST & OpenAPI', 'Built-in auth', 'HTTPS'],
note: 'Replace the example endpoint with your weather API.',
...transportExample({
type: 'http', name: 'api', tool: 'get_weather', description: 'Get the weather for a location.',
properties: { location: { type: 'string' }, units: { type: 'string', enum: ['metric', 'imperial'] } },
args: { location: 'Warsaw', units: 'metric' },
template: { url: 'https://api.example.com/weather/{location}', http_method: 'GET' },
}),
},
{
id: 'sse', name: 'SSE', icon: 'radio', category: 'Keep the conversation flowing',
title: 'Events as they happen.',
description: 'Subscribe to Server-Sent Events with a familiar Ruby enumerator. Filter events and process a stream one item at a time.',
tags: ['Server-Sent Events', 'Enumerators', 'Event filters'],
note: 'The example endpoint closes after the requested event limit.',
...transportExample({
type: 'sse', name: 'events', tool: 'watch', description: 'Stream a limited number of events for a topic.', streaming: true,
properties: { topic: { type: 'string' }, limit: { type: 'integer', minimum: 1 } },
args: { topic: 'builds', limit: 3 },
template: { url: 'https://api.example.com/events/{topic}', reconnect: false, total_timeout: 10 },
}),
},
{
id: 'streamable_http', name: 'Streamable HTTP', icon: 'stream', category: 'A little at a time',
title: 'Built for the stream.',
description: 'Work with NDJSON, JSON sequences, and binary chunks over HTTP. Consume incremental results without changing your client.',
tags: ['NDJSON', 'JSON Sequence', 'Binary chunks'],
note: 'The example endpoint returns NDJSON and closes when complete.',
...transportExample({
type: 'streamable_http', name: 'stream', tool: 'tokens', description: 'Stream generated tokens for a prompt.', streaming: true,
properties: { prompt: { type: 'string' }, max_tokens: { type: 'integer', minimum: 1 } },
args: { prompt: 'Explain UTCP', max_tokens: 32 },
template: { url: 'https://api.example.com/tokens', http_method: 'GET', chunk_size: 4096, total_timeout: 10 },
}),
},
{
id: 'websocket', name: 'WebSocket', icon: 'activity', category: 'A connection that stays open',
title: 'Stay in the loop.',
description: 'Keep a persistent connection to your tools. Native WebSocket support handles TLS, framing, and ping/pong for you.',
tags: ['Persistent connections', 'WSS', 'JSON & binary'],
note: 'Use WSS for remote connections.',
...transportExample({
type: 'websocket', name: 'realtime', tool: 'echo', description: 'Echo a message over a persistent connection.',
properties: { message: { type: 'string' } }, args: { message: 'Hello from Ruby!' },
template: { url: 'wss://api.example.com/echo', keep_alive: true, response_format: 'json' },
}),
},
{
id: 'grpc', name: 'gRPC', icon: 'layers', category: 'A compact, typed connection',
title: 'Meet your RPC services.',
description: 'Discover and invoke tools through the UTCP protobuf service. Unary calls and server streams share the same Ruby interface.',
tags: ['Protobuf', 'Server streaming', 'UTCPService'],
note: 'Requires the grpc gem and a UTCPService server stream.',
...transportExample({
type: 'grpc', name: 'rpc', tool: 'watch', description: 'Stream a limited number of topic updates.', streaming: true,
properties: { topic: { type: 'string' }, limit: { type: 'integer', minimum: 1 } },
args: { topic: 'builds', limit: 3 },
template: { host: 'api.example.com', port: 443, use_ssl: true, service_name: 'grpcpb.UTCPService' },
}),
},
{
id: 'graphql', name: 'GraphQL', icon: 'graphql', category: 'Your schema is the starting point',
title: 'From schema to tools.',
description: 'Discover operations through introspection. Queries, mutations, and subscriptions become tools with their own input and output schemas.',
tags: ['Introspection', 'Queries & mutations', 'Subscriptions'],
note: 'Use a schema with an events(topic, limit) subscription.',
...transportExample({
type: 'graphql', name: 'graph', tool: 'events', description: 'Subscribe to a limited number of topic events.', streaming: true,
properties: { topic: { type: 'string' }, limit: { type: 'integer', minimum: 1 } },
args: { topic: 'builds', limit: 3 },
template: { url: 'https://api.example.com/graphql', operation_type: 'subscription', operation_name: 'events', variable_types: { topic: 'String!', limit: 'Int!' }, selection_set: 'id status' },
}),
},
{
id: 'cli', name: 'CLI', icon: 'terminal', category: 'The command line is an API, too',
title: 'Put your scripts to work.',
description: 'Discover manuals from a command and call local tools with safe argument interpolation. Multi-step commands can share a working directory.',
tags: ['Local commands', 'Argument interpolation', 'Multi-step calls'],
note: 'The name parameter is safely interpolated into the command.',
...transportExample({
type: 'cli', name: 'shell', tool: 'greet', description: 'Print a greeting for a name.',
properties: { name: { type: 'string' } }, args: { name: 'Ruby' },
template: { commands: [{ command: "printf 'Hello, %s!\\n' UTCP_ARG_name_UTCP_END" }] },
}),
},
{
id: 'tcp', name: 'TCP', icon: 'network', category: 'Down to the socket',
title: 'A direct line to tools.',
description: 'Talk to TCP services with configurable message framing. Use length prefixes, delimiters, fixed sizes, or a continuous stream.',
tags: ['Socket transport', 'Flexible framing', 'JSON & text'],
note: 'Connect to a JSON echo server using length-prefix framing.',
...transportExample({
type: 'tcp', name: 'socket', tool: 'echo', description: 'Echo a JSON message over TCP.',
properties: { message: { type: 'string' } }, args: { message: 'Hello over TCP' },
template: { host: 'localhost', port: 9000, framing_strategy: 'length_prefix', request_data_format: 'json' },
}),
},
{
id: 'udp', name: 'UDP', icon: 'send', category: 'Small messages, simple calls',
title: 'Tools by datagram.',
description: 'Send arguments over UDP and choose how many response datagrams to collect. Set the count to zero for fire-and-forget tools.',
tags: ['Datagrams', 'Configurable responses', 'JSON & text'],
note: 'Connect to a JSON echo server that returns one datagram.',
...transportExample({
type: 'udp', name: 'datagrams', tool: 'echo', description: 'Echo a JSON message over UDP.',
properties: { message: { type: 'string' } }, args: { message: 'Hello over UDP' },
template: { host: 'localhost', port: 9001, number_of_response_datagrams: 1, request_data_format: 'json' },
}),
},
{
id: 'webrtc', name: 'WebRTC', icon: 'peers', category: 'Make a peer-to-peer connection',
title: 'Let your peers talk.',
description: 'Exchange tool calls over WebRTC DataChannels. Signaling establishes the connection, and request IDs match each call to its result.',
tags: ['DataChannels', 'Peer connections', 'Custom adapters'],
note: 'Default backend: Ruby 3.1+, webrtc-ruby, libdatachannel.',
...transportExample({
type: 'webrtc', name: 'peer', tool: 'echo', description: 'Echo a message through a peer DataChannel.',
properties: { message: { type: 'string' } }, args: { message: 'Hello from this peer' },
template: { signaling_server: 'https://api.example.com', peer_id: 'ruby-client', data_channel_name: 'utcp' },
}),
},
{
id: 'mcp', name: 'MCP', icon: 'connect', category: 'Bring your existing tools along',
title: 'An open door to MCP.',
description: 'Connect to MCP servers over stdio or Streamable HTTP. Discover tools and call them alongside your other native integrations.',
tags: ['stdio & HTTP', 'Tool discovery', 'Session isolation'],
note: 'Match the tool name and inputs exposed by your MCP server.',
...transportExample({
type: 'mcp', name: 'bridge', tool: 'remote.get_weather', description: 'Call the remote MCP weather tool.',
properties: { location: { type: 'string' } }, args: { location: 'Warsaw' },
template: { name: 'bridge', config: { mcpServers: { remote: { transport: 'http', url: 'https://mcp.example.com/mcp' } } } },
}),
},
{
id: 'text', name: 'Text', icon: 'file', category: 'Sometimes simple is perfect',
title: 'Start with a document.',
description: 'Load a UTCP or OpenAPI manual directly from a string. Static text tools are a handy starting point for local experiments and tests.',
tags: ['Inline manuals', 'Static tools', 'No network required'],
note: 'Text tools return static content; their input schema is empty.',
...transportExample({
type: 'text', name: 'local', tool: 'hello', description: 'Return a static greeting without parameters.',
properties: {}, args: {}, template: { content: 'Hello from Ruby UTCP!' },
}),
},
];
export const localExample = [
'require "json"',
'require "utcp"',
'',
'manual = {',
' utcp_version: "1.1.0",',
' manual_version: "1.0.0",',
' tools: [{',
' name: "hello",',
' description: "A friendly first tool",',
' tool_call_template: {',
' call_template_type: "text",',
' content: "Hello from Ruby UTCP!"',
' }',
' }]',
'}',
'',
'client = UTCP::Client.create(config: {',
' manual_call_templates: [{',
' name: "local", call_template_type: "text",',
' content: JSON.generate(manual)',
' }]',
'})',
'',
'puts client.call_tool("local.hello")',
'client.close',
].join('\n');
export const docs = [
{
id: 'quickstart', name: 'Getting started', icon: 'code',
description: 'Install the gem and make your first tool call.',
keywords: 'install installation bundler rubygems setup quick start gem ruby hello',
intro: 'A small gem that connects Ruby apps and AI agents to tools over their native protocols. Let’s make your first connection.',
blocks: [
{ type: 'heading', title: 'Install Ruby UTCP' },
{ type: 'paragraph', text: 'Add the gem to your Gemfile, then run <code>bundle install</code>.' },
{ type: 'code', label: 'Gemfile', code: 'gem "ruby-utcp"' },
{ type: 'paragraph', text: 'You can also install it directly with RubyGems.' },
{ type: 'code', label: 'Terminal', code: 'gem install ruby-utcp' },
{ type: 'callout', title: 'A familiar foundation', text: 'The core and 11 transports support Ruby 2.6+. The default WebRTC backend needs Ruby 3.1+, <code>webrtc-ruby</code>, and <code>libdatachannel</code>. gRPC loads its optional <code>grpc</code> gem only when used.' },
{ type: 'heading', title: 'Connect to a manual' },
{ type: 'paragraph', text: 'A UTCP manual describes the tools a service provides. Create a client with its discovery endpoint. <code>Client.create</code> registers configured manuals before returning.' },
{ type: 'code', label: 'app.rb', code: quickstart },
{ type: 'paragraph', text: 'Replace the example URL with your own manual endpoint and use a tool name from that manual. The weather endpoint above is a placeholder. Registered names use <code>manual_name.tool_name</code>.' },
{ type: 'heading', title: 'Try it without a server' },
{ type: 'paragraph', text: 'This complete example registers a local text tool. Save it as <code>hello.rb</code> and run <code>bundle exec ruby hello.rb</code> after installing the gem.' },
{ type: 'code', label: 'hello.rb', code: localExample },
{ type: 'code', label: 'Output', code: 'Hello from Ruby UTCP!' },
{ type: 'paragraph', text: 'Ruby calls are synchronous: registration, search, and tool calls return their results directly. Close the client when you finish to release any connections and sessions.' },
],
},
{
id: 'discovery', name: 'Discovery & search', icon: 'scan',
description: 'Load manuals, discover tools, and find the right one.',
keywords: 'manual openapi swagger tool search repository tags list discover',
intro: 'Describe what a tool does and how to call it. Your client takes care of turning that description into a usable connection.',
blocks: [
{ type: 'heading', title: 'What is a manual?' },
{ type: 'paragraph', text: 'A UTCP manual contains protocol and manual versions, tool descriptions, input and output schemas, and a call template for each tool. Every tool can describe its own native endpoint.' },
{ type: 'code', label: 'manual.json', code: '{\n "utcp_version": "1.1.0",\n "manual_version": "1.0.0",\n "tools": [{\n "name": "hello",\n "description": "Return a friendly message",\n "tool_call_template": {\n "call_template_type": "text",\n "content": "Hello from Ruby!"\n }\n }]\n}' },
{ type: 'heading', title: 'Discover the available tools' },
{ type: 'paragraph', text: 'Once a manual is registered, inspect the tools in the local repository. Tool names are qualified by their manual, so a tool named <code>hello</code> in the <code>local</code> manual becomes <code>local.hello</code>.' },
{ type: 'code', label: 'discover.rb · after client creation', code: 'client.list_tools.each do |tool|\n puts tool.name\n puts tool.description\nend' },
{ type: 'heading', title: 'Find the right tool' },
{ type: 'paragraph', text: 'Local search ranks matches in tags, names, and descriptions. Limit the number of results or require at least one of a set of tags.' },
{ type: 'code', label: 'search.rb · after client creation', code: 'tools = client.search_tools(\n "weather forecast",\n limit: 5,\n any_of_tags_required: ["weather"]\n)\n\nputs tools.map(&:name)' },
{ type: 'heading', title: 'Already have an OpenAPI document?' },
{ type: 'paragraph', text: 'HTTP, text, and file manuals can also contain OpenAPI 3 or Swagger 2 documents. Ruby UTCP converts their operations into tools automatically, so your existing API description can be the starting point.' },
{ type: 'callout', title: 'Discovery respects protocol boundaries', text: 'By default, a manual can only register tools that use its own protocol. Mixed-protocol manuals must opt in with <code>allowed_communication_protocols</code>. See <a href="./docs.html?topic=security">security & limits</a>.' },
],
},
{
id: 'transports', name: 'Transports', icon: 'network',
description: 'Explore all 12 native communication protocols.',
keywords: 'http sse streamable websocket grpc graphql cli tcp udp webrtc mcp text file protocol',
intro: 'One consistent Ruby client, twelve native transports. Choose the connection your tool already understands.',
blocks: [
{ type: 'paragraph', text: 'Each manual has a <code>call_template_type</code>. That type selects its communication protocol, while individual tool call templates describe the endpoint and parameters used for each call.' },
{ type: 'transports' },
{ type: 'heading', title: 'Local file manuals' },
{ type: 'paragraph', text: 'The <code>file</code> transport is an additional Ruby extension. It reads JSON or safe YAML relative to the client’s <code>root_dir</code>. Explicitly allow any other protocols used by tools in that file.' },
{ type: 'code', label: 'local.rb', code: 'require "utcp"\n\nclient = UTCP::Client.create(config: {\n manual_call_templates: [{\n name: "local",\n call_template_type: "file",\n file_path: "tools.json",\n allowed_communication_protocols: ["http"]\n }]\n})\n\nputs client.list_tools.map(&:name)\nclient.close' },
{ type: 'heading', title: 'Run the examples' },
{ type: 'paragraph', text: 'The repository includes a client example for every transport, with matching local servers for network protocols. Start with the <a href="https://github.com/universal-tool-calling-protocol/ruby-utcp/tree/main/examples">examples directory</a>.' },
{ type: 'code', label: 'Terminal · in the repository', code: '# Run pairs without native backends\nmake standard-demo\n\n# Strictly run all twelve transports\nmake full-demo' },
{ type: 'callout', title: 'Optional native backends', text: 'gRPC requires the <code>grpc</code> gem. The default WebRTC backend requires Ruby 3.1+, <code>webrtc-ruby</code>, and <code>libdatachannel</code>. Applications can supply custom adapters instead.' },
],
},
{
id: 'authentication', name: 'Authentication', icon: 'key',
description: 'Connect with API keys, Basic auth, and OAuth2.',
keywords: 'auth api key oauth oauth2 bearer basic secret token variables environment env',
intro: 'Keep credentials in your environment and configure authentication where your tool’s protocol supports it.',
blocks: [
{ type: 'heading', title: 'API key authentication' },
{ type: 'paragraph', text: 'HTTP-family transports can put an API key in a header, query parameter, or cookie. This example uses an Authorization header and resolves the token from the environment.' },
{ type: 'code', label: 'authenticated.rb', code: 'require "utcp"\n\nclient = UTCP::Client.create(config: {\n manual_call_templates: [{\n name: "api",\n call_template_type: "http",\n url: "https://api.example.com/utcp",\n auth: {\n auth_type: "api_key",\n api_key: ENV.fetch("API_TOKEN"),\n var_name: "Authorization",\n location: "header"\n }\n }]\n})\n\nputs client.list_tools.map(&:name)\nclient.close' },
{ type: 'heading', title: 'Protocol support' },
{ type: 'list', items: ['HTTP, SSE, Streamable HTTP, WebSocket, GraphQL, gRPC, and MCP over HTTP support API key, Basic, and OAuth2 authentication with protocol-specific details.', 'gRPC maps API keys with <code>location: "header"</code> into metadata.', 'MCP stdio and CLI tools use their process environment for credentials. They reject the transport <code>auth</code> field.', 'TCP, UDP, and WebRTC reject built-in transport authentication configuration. Application-level authentication is caller-defined.'] },
{ type: 'heading', title: 'Variables and environment files' },
{ type: 'paragraph', text: 'Variable placeholders resolve from configuration variables first, then configured loaders, then the process environment. Dotenv loaders can read application environment files.' },
{ type: 'code', label: 'Variable configuration', code: 'config = {\n variables: { HOST: "api.example.com" },\n load_variables_from: [{\n variable_loader_type: "dotenv",\n env_file_path: ".env"\n }]\n}' },
{ type: 'callout', title: 'Credentials stay with their origin', text: 'Cross-origin HTTP redirects strip credentials. OAuth2 token requests only follow redirects within the same origin. For the complete support matrix, see the <a href="https://github.com/universal-tool-calling-protocol/ruby-utcp#authentication-by-transport">authentication reference</a>.' },
],
},
{
id: 'security', name: 'Security & limits', icon: 'shield',
description: 'Understand protocol allow-lists and response budgets.',
keywords: 'secure security allowlist allow-list https timeout response limits bytes protocol rules 1.1',
intro: 'UTCP 1.1 makes protocol boundaries explicit, with checks during discovery and on every tool call.',
blocks: [
{ type: 'heading', title: 'Protocols are opt-in' },
{ type: 'paragraph', text: 'An HTTP manual can register HTTP tools by default. Other protocols are filtered out unless the manual explicitly allows them. An omitted or empty allow-list means only the manual’s own protocol is permitted.' },
{ type: 'code', label: 'An explicitly mixed-protocol manual', code: 'template = {\n name: "mixed_tools",\n call_template_type: "http",\n url: "https://api.example.com/utcp",\n allowed_communication_protocols: ["http", "cli"]\n}' },
{ type: 'paragraph', text: 'Only allow CLI tools from manuals you trust: their command templates execute code. The client enforces the allow-list both when registering tools and when invoking them.' },
{ type: 'heading', title: 'Bound incoming responses' },
{ type: 'paragraph', text: 'Network transports default to a 100 MiB response budget. Set <code>max_response_bytes</code> to a positive value to change it. Exceeding the budget raises a UTCP error; responses are never silently truncated. File, CLI, and text do not use this network response limit.' },
{ type: 'code', label: 'response_limits.rb', code: 'require "utcp"\n\ntemplate = UTCP::HttpCallTemplate.new(\n url: "https://api.example.com/utcp",\n max_response_bytes: 2 * 1024 * 1024,\n total_timeout: 30\n)' },
{ type: 'heading', title: 'Secure remote connections' },
{ type: 'list', items: ['Remote HTTP endpoints require HTTPS. Plain HTTP is accepted for loopback development hosts.', 'Remote WebSocket connections use WSS. Plain WS is restricted to literal loopback hosts.', 'Redirect targets are validated again; credentials are stripped on cross-origin redirects.', 'SSE and Streamable HTTP also bound individual event size and the number of emitted items.'] },
{ type: 'callout', title: 'Choose limits for your workload', text: 'A network response budget does not replace endpoint authentication or application authorization. For streaming timeouts, see <a href="./docs.html?topic=streaming">working with streams</a>.' },
],
},
{
id: 'streaming', name: 'Working with streams', icon: 'radio',
description: 'Consume incremental results with Ruby enumerators.',
keywords: 'stream streaming enumerator sse ndjson graphql subscription grpc event timeout',
intro: 'Consume events, tokens, or chunks as they arrive. Streaming tools return ordinary Ruby enumerators.',
blocks: [
{ type: 'heading', title: 'Read a streaming tool' },
{ type: 'paragraph', text: 'After registering your manual, call <code>call_tool_streaming</code> and iterate over the results. SSE, Streamable HTTP, GraphQL subscriptions, and gRPC server streams expose this interface.' },
{ type: 'code', label: 'stream.rb · after client creation', code: 'client.call_tool_streaming(\n "events.watch", topic: "builds"\n).each do |event|\n puts event.inspect\nend' },
{ type: 'heading', title: 'Give a stream a deadline' },
{ type: 'paragraph', text: 'For direct HTTP stream enumeration, configure <code>total_timeout</code> to set an overall deadline in seconds. The read timeout still applies between reads.' },
{ type: 'code', label: 'A bounded SSE manual template', code: 'template = {\n name: "events",\n call_template_type: "sse",\n url: "https://api.example.com/utcp",\n total_timeout: 60,\n max_event_bytes: 1_048_576,\n max_response_items: 10_000\n}' },
{ type: 'heading', title: 'Finish cleanly' },
{ type: 'paragraph', text: 'Breaking out of an HTTP stream, GraphQL subscription, or gRPC stream releases its connection or cancels the RPC. Close your client when its work is finished.' },
{ type: 'code', label: 'Resource lifecycle · after client creation', code: 'begin\n client.call_tool_streaming("events.watch").each do |event|\n puts event\n break\n end\nensure\n client.close\nend' },
{ type: 'callout', title: 'Budgets span the stream', text: 'For SSE, Streamable HTTP, gRPC streams, and GraphQL subscriptions, the response byte budget is shared across response items. Plan the budget around the lifetime and size of your stream.' },
],
},
{
id: 'code-mode', name: 'Code Mode', icon: 'code',
description: 'Run multi-step Ruby workflows with an OpenRouter model.',
keywords: 'codemode code mode openrouter llm ai agent workflow execute chain model',
intro: 'Let a model compose several tool calls into one small Ruby program. Code Mode runs the workflow through a constrained interpreter and returns its result and logs.',
blocks: [
{ type: 'heading', title: 'A workflow, in one call' },
{ type: 'paragraph', text: 'Create a <code>UTCP::CodeModeUtcpClient</code> with the same manual configuration as a regular client. Inside a workflow, call tools through <code>codemode.call_tool</code>. The final expression becomes the result.' },
{ type: 'code', label: 'A workflow · with the demo client below', code: 'execution = client.call_tool_chain(<<~\'RUBY\', timeout: 10)\n matches = codemode.search_tools("greeting", limit: 1)\n tool_name = matches.first["name"]\n first = codemode.call_tool(tool_name)\n second = codemode.call_tool(tool_name)\n greetings = [first, second].map { |item| item.upcase }\n puts "Called the greeting tool twice"\n { greetings: greetings, count: greetings.length }\nRUBY\n\nputs execution["result"]\nputs execution["logs"]' },
{ type: 'heading', title: 'Connect Code Mode to OpenRouter' },
{ type: 'paragraph', text: 'Expose a single <code>execute_ruby_workflow</code> function to the model. Your Ruby application executes the returned code through Code Mode and sends the result back with the matching <code>tool_call_id</code>. The model then writes its answer. This follows the <a href="https://openrouter.ai/docs/guides/features/tool-calling">OpenRouter tool-calling interface</a>.' },
{ type: 'flow', items: ['OpenRouter model', 'Ruby workflow', 'UTCP tools', 'Result + answer'] },
{ type: 'heading', title: 'Prompt for Ruby Code Mode source' },
{ type: 'paragraph', text: 'The system prompt explicitly asks the model to generate Ruby Code Mode code. It includes a complete discovery, tool-call, and transformation example, the runtime API instructions, and the registered tool interfaces. The model returns raw source in the function’s <code>code</code> argument; the host prints that source and passes it to <code>call_tool_chain</code>.' },
{ type: 'code', label: 'The code-generation instruction', code: 'Generate a Ruby Code Mode program for this task:\nDiscover the greeting tool, call it twice, uppercase both\ngreetings, and return the greetings and their count.\n\nReturn raw Ruby in execute_ruby_workflow.code.\nDiscover with codemode.search_tools.\nCall tools with codemode.call_tool.\nCollect streaming tools with codemode.call_tool_stream.\nDo not include Markdown fences, require, or client setup.' },
{ type: 'heading', title: 'Generate streaming workflows, too' },
{ type: 'paragraph', text: 'In ordinary Ruby, use <code>client.call_tool_streaming</code> and consume its enumerator. This method is also inherited by <code>CodeModeUtcpClient</code> for calls made outside a workflow.' },
{ type: 'code', label: 'Host Ruby · after creating a client', code: 'client.call_tool_streaming(\n "events.watch", topic: "builds"\n).each do |event|\n puts event.inspect\nend' },
{ type: 'paragraph', text: 'Inside the generated Code Mode program, use <code>codemode.call_tool_stream</code>. It collects the stream into an array under the runtime’s value and time limits. The host <code>client</code> object is outside this runtime’s scope. The OpenRouter prompt explicitly explains this mapping. Use the exact name and inputs of a registered streaming tool.' },
{ type: 'code', label: 'Streaming Code Mode · illustrative tool name', code: 'chunks = codemode.call_tool_stream(\n "events.watch", topic: "builds"\n)\n\n{ chunks: chunks, count: chunks.length }' },
{ type: 'heading', title: 'Run the streaming examples in the Code Mode file' },
{ type: 'paragraph', text: 'The complete <code>openrouter_code_mode.rb</code> example below includes executable <code>client.call_tool_streaming</code> examples using both an enumerator and a direct block, plus a streaming Code Mode workflow. Run all three locally without an OpenRouter API key.' },
{ type: 'code', label: 'Terminal · after downloading the example', code: 'ruby openrouter_code_mode.rb --streaming-examples' },
{ type: 'code', label: 'Inside openrouter_code_mode.rb', code: '# Enumerator form\nclient.call_tool_streaming("demo.greeting").each do |chunk|\n puts chunk.upcase\nend\n\n# Direct block form\nclient.call_tool_streaming("demo.greeting") do |chunk|\n puts chunk.upcase\nend\n\n# Interpreted Code Mode workflow\nexecution = client.call_tool_chain(<<~\'RUBY\', timeout: 10)\n chunks = codemode.call_tool_stream("demo.greeting")\n greetings = chunks.map { |chunk| chunk.upcase }\n { greetings: greetings, count: greetings.length }\nRUBY\n\nputs execution["result"]' },
{ type: 'paragraph', text: 'The local text tool yields one item through the streaming interface. A registered SSE, Streamable HTTP, or other streaming tool uses the same client API for multiple items.' },
{ type: 'heading', title: '1. Install and configure' },
{ type: 'paragraph', text: 'Install the gem, set your API key, and choose a model from <a href="https://openrouter.ai/models?supported_parameters=tools">OpenRouter’s tool-capable models</a>. Set <code>OPENROUTER_MODEL</code> to its exact model ID. The example uses Ruby’s HTTP library and a local text tool, so a separate tool server is not needed.' },
{ type: 'code', label: 'Terminal', code: 'gem install ruby-utcp\nexport OPENROUTER_API_KEY="your-openrouter-key"\nexport OPENROUTER_MODEL="your-selected-model-id"\nruby openrouter_code_mode.rb' },
{ type: 'paragraph', text: 'The model and key above are placeholders. Running the script sends prompts, tool descriptions, and results to OpenRouter and the selected provider, and uses your account’s API credits. Keep the API key in the server-side environment.' },
{ type: 'heading', title: '2. Run the complete example' },
{ type: 'paragraph', text: '<a href="./examples/openrouter_code_mode.rb" download>Download openrouter_code_mode.rb</a>, or copy the source below. It registers <code>demo.greeting</code>, prompts for Ruby Code Mode code, prints and executes the generated program, returns its result to the model, and closes the client when finished.' },
{ type: 'source', label: 'openrouter_code_mode.rb', src: './examples/openrouter_code_mode.rb' },
{ type: 'heading', title: '3. Bring your own tools' },
{ type: 'paragraph', text: 'Replace the local manual in <code>build_client</code> with your own HTTP manual configuration. Change the prompt to match the tools you register. The OpenRouter conversation loop stays the same.' },
{ type: 'code', label: 'Replace the demo client configuration', code: 'UTCP::CodeModeUtcpClient.create(config: {\n manual_call_templates: [{\n name: "api",\n call_template_type: "http",\n url: ENV.fetch("UTCP_MANUAL_URL")\n }]\n})' },
{ type: 'heading', title: 'Discover tools progressively' },
{ type: 'paragraph', text: 'Use <code>get_all_tools_ruby_interfaces</code> to inspect the catalog outside a workflow. Within Code Mode, <code>codemode.search_tools</code>, <code>codemode.get_tool_interface</code>, and <code>codemode.interfaces</code> support discovery. <code>codemode.call_tool_stream</code> collects a streaming tool into an array.' },
{ type: 'callout', title: 'A constrained runtime, with explicit tool access', text: 'Code Mode supports local variables, JSON-like values, arithmetic, control flow, and common collection transforms. It does not expose filesystem, process, imports, reflection, eval, or direct network APIs. Registered tools can still have external effects. The example limits each workflow to 10 seconds and 10,000 interpreter steps, with at most four model requests.' },
{ type: 'heading', title: 'References' },
{ type: 'paragraph', text: 'See <a href="https://www.utcp.io/">utcp.io</a> for the official Universal Tool Calling Protocol documentation, the <a href="https://github.com/universal-tool-calling-protocol/ruby-utcp#code-mode">Ruby UTCP Code Mode reference</a> for runtime limits, and the <a href="https://openrouter.ai/docs/quickstart">OpenRouter quick start</a> for API configuration.' },
],
},
{
id: 'contributing', name: 'Contributing', icon: 'github',
description: 'Run examples, report issues, and contribute to the gem.',
keywords: 'contribute contributing github source license mit community tests examples issues',
intro: 'Ruby UTCP is open source under the MIT license. Improvements, practical examples, and thoughtful bug reports are welcome.',
blocks: [
{ type: 'heading', title: 'Explore the project' },
{ type: 'paragraph', text: 'The <a href="https://github.com/universal-tool-calling-protocol/ruby-utcp">GitHub repository</a> contains the client, protocol implementations, tests, and runnable examples. Start with an example close to your use case.' },
{ type: 'code', label: 'Terminal', code: 'git clone https://github.com/universal-tool-calling-protocol/ruby-utcp.git\ncd ruby-utcp\nbundle install\nbundle exec rake test' },
{ type: 'heading', title: 'Ways to help' },
{ type: 'list', items: ['Report a reproducible issue with the Ruby version, transport, and a minimal example. Remove credentials before sharing logs or configuration.', 'Improve an example or clarify the documentation.', 'Add a regression test with a focused fix.', 'Discuss a new transport or extension before taking on a larger change.'] },
{ type: 'heading', title: 'Try the local examples' },
{ type: 'paragraph', text: 'The standard demo runs local client/server pairs that do not require native backends. The full demo requires all optional transport dependencies.' },
{ type: 'code', label: 'Terminal · in the repository', code: 'make standard-demo\nmake test' },
{ type: 'paragraph', text: 'Browse <a href="https://github.com/universal-tool-calling-protocol/ruby-utcp/issues">issues on GitHub</a> or learn more about the broader <a href="https://www.utcp.io">Universal Tool Calling Protocol</a>.' },
],
},
];