robot_lab 0.3.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +60 -0
- data/README.md +29 -19
- data/lib/robot_lab/errors.rb +4 -0
- data/lib/robot_lab/hook_context.rb +9 -2
- data/lib/robot_lab/mcp/registry.rb +99 -0
- data/lib/robot_lab/mcp/server.rb +47 -83
- data/lib/robot_lab/mcp/server_discovery.rb +22 -9
- data/lib/robot_lab/mcp.rb +259 -0
- data/lib/robot_lab/robot/mcp_management.rb +159 -105
- data/lib/robot_lab/robot/tool_call_gating.rb +144 -0
- data/lib/robot_lab/robot.rb +72 -99
- data/lib/robot_lab/run_config.rb +3 -2
- data/lib/robot_lab/tool.rb +8 -23
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +1 -4
- metadata +16 -292
- data/.architecture/AGENTS.md +0 -32
- data/.architecture/config.yml +0 -8
- data/.architecture/members.yml +0 -60
- data/.architecture/reviews/feature-free-will.md +0 -490
- data/.architecture/reviews/overall-codebase.md +0 -427
- data/.claude/settings.local.json +0 -57
- data/.codex/config.toml +0 -2
- data/.envrc +0 -6
- data/.github/workflows/deploy-github-pages.yml +0 -52
- data/.irbrc +0 -6
- data/.loki +0 -65
- data/.quality/reek_baseline.txt +0 -43
- data/.rubocop.yml +0 -10
- data/Archspec.rb +0 -44
- data/CLAUDE.md +0 -141
- data/COMMITS.md +0 -196
- data/Rakefile +0 -193
- data/_typos.toml +0 -21
- data/agent2agent_review.md +0 -192
- data/agentf_improvements.md +0 -253
- data/agents.md +0 -14
- data/docs/api/core/index.md +0 -147
- data/docs/api/core/memory.md +0 -578
- data/docs/api/core/network.md +0 -553
- data/docs/api/core/result.md +0 -211
- data/docs/api/core/robot.md +0 -1667
- data/docs/api/core/state.md +0 -145
- data/docs/api/core/tool.md +0 -520
- data/docs/api/errors.md +0 -323
- data/docs/api/hooks.md +0 -469
- data/docs/api/index.md +0 -137
- data/docs/api/mcp/client.md +0 -387
- data/docs/api/mcp/index.md +0 -267
- data/docs/api/mcp/server.md +0 -252
- data/docs/api/mcp/transports.md +0 -339
- data/docs/api/messages/index.md +0 -126
- data/docs/api/messages/text-message.md +0 -170
- data/docs/api/messages/tool-call-message.md +0 -206
- data/docs/api/messages/tool-result-message.md +0 -242
- data/docs/api/messages/user-message.md +0 -232
- data/docs/api/skills.md +0 -423
- data/docs/api/streaming/context.md +0 -280
- data/docs/api/streaming/events.md +0 -185
- data/docs/api/streaming/index.md +0 -156
- data/docs/api/support.md +0 -846
- data/docs/architecture/core-concepts.md +0 -575
- data/docs/architecture/index.md +0 -252
- data/docs/architecture/message-flow.md +0 -366
- data/docs/architecture/network-orchestration.md +0 -478
- data/docs/architecture/robot-execution.md +0 -396
- data/docs/architecture/state-management.md +0 -410
- data/docs/assets/css/custom.css +0 -56
- data/docs/assets/images/robot_lab.jpg +0 -0
- data/docs/concepts.md +0 -641
- data/docs/examples/basic-chat.md +0 -258
- data/docs/examples/index.md +0 -425
- data/docs/examples/mcp-server.md +0 -403
- data/docs/examples/multi-robot-network.md +0 -476
- data/docs/examples/tool-usage.md +0 -468
- data/docs/getting-started/configuration.md +0 -645
- data/docs/getting-started/index.md +0 -56
- data/docs/getting-started/installation.md +0 -197
- data/docs/getting-started/quick-start.md +0 -293
- data/docs/guides/building-robots.md +0 -1187
- data/docs/guides/creating-networks.md +0 -736
- data/docs/guides/hooks.md +0 -1206
- data/docs/guides/index.md +0 -78
- data/docs/guides/knowledge.md +0 -219
- data/docs/guides/mcp-integration.md +0 -740
- data/docs/guides/memory.md +0 -458
- data/docs/guides/observability.md +0 -733
- data/docs/guides/streaming.md +0 -323
- data/docs/guides/using-tools.md +0 -780
- data/docs/index.md +0 -280
- data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
- data/examples/.envrc +0 -3
- data/examples/01_simple_robot.rb +0 -31
- data/examples/02_tools.rb +0 -109
- data/examples/03_network.rb +0 -118
- data/examples/04_mcp.rb +0 -217
- data/examples/05_streaming.rb +0 -137
- data/examples/06_prompt_templates.rb +0 -325
- data/examples/07_network_memory.rb +0 -313
- data/examples/08_llm_config.rb +0 -162
- data/examples/09_chaining.rb +0 -251
- data/examples/10_memory.rb +0 -315
- data/examples/11_network_introspection.rb +0 -239
- data/examples/12_message_bus.rb +0 -67
- data/examples/13_spawn.rb +0 -85
- data/examples/14_rusty_circuit/.envrc +0 -1
- data/examples/14_rusty_circuit/.gitignore +0 -1
- data/examples/14_rusty_circuit/README.md +0 -1
- data/examples/14_rusty_circuit/comic.rb +0 -149
- data/examples/14_rusty_circuit/display.rb +0 -217
- data/examples/14_rusty_circuit/heckler.rb +0 -65
- data/examples/14_rusty_circuit/open_mic.rb +0 -132
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
- data/examples/14_rusty_circuit/scout.rb +0 -165
- data/examples/14_rusty_circuit/show.log +0 -234
- data/examples/15_memory_network_and_bus/.envrc +0 -1
- data/examples/15_memory_network_and_bus/README.md +0 -66
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
- data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
- data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
- data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
- data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
- data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/memory.json +0 -13
- data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
- data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
- data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
- data/examples/16_writers_room/.envrc +0 -1
- data/examples/16_writers_room/display.rb +0 -158
- data/examples/16_writers_room/output/.gitignore +0 -4
- data/examples/16_writers_room/output/README.md +0 -69
- data/examples/16_writers_room/output/opus_001.md +0 -263
- data/examples/16_writers_room/output/opus_001_notes.log +0 -470
- data/examples/16_writers_room/output/opus_002.md +0 -245
- data/examples/16_writers_room/output/opus_002_notes.log +0 -546
- data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
- data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
- data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
- data/examples/16_writers_room/prompts/writer.md +0 -37
- data/examples/16_writers_room/room.rb +0 -186
- data/examples/16_writers_room/tools.rb +0 -173
- data/examples/16_writers_room/writer.rb +0 -121
- data/examples/16_writers_room/writers_room.rb +0 -256
- data/examples/17_skills.rb +0 -234
- data/examples/18_rails/.envrc +0 -3
- data/examples/18_rails/.gitignore +0 -5
- data/examples/18_rails/Gemfile +0 -11
- data/examples/18_rails/README.md +0 -67
- data/examples/18_rails/Rakefile +0 -4
- data/examples/18_rails/app/controllers/application_controller.rb +0 -4
- data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
- data/examples/18_rails/app/jobs/application_job.rb +0 -4
- data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
- data/examples/18_rails/app/models/application_record.rb +0 -5
- data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
- data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
- data/examples/18_rails/app/robots/chat_robot.rb +0 -25
- data/examples/18_rails/app/tools/time_tool.rb +0 -9
- data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
- data/examples/18_rails/app/views/chat/index.html.erb +0 -67
- data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
- data/examples/18_rails/bin/dev +0 -7
- data/examples/18_rails/bin/rails +0 -6
- data/examples/18_rails/bin/setup +0 -15
- data/examples/18_rails/config/application.rb +0 -33
- data/examples/18_rails/config/cable.yml +0 -2
- data/examples/18_rails/config/database.yml +0 -5
- data/examples/18_rails/config/environment.rb +0 -4
- data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
- data/examples/18_rails/config/routes.rb +0 -6
- data/examples/18_rails/config.ru +0 -4
- data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
- data/examples/19_token_tracking.rb +0 -138
- data/examples/20_circuit_breaker.rb +0 -147
- data/examples/21_learning_loop.rb +0 -181
- data/examples/22_context_compression.rb +0 -172
- data/examples/23_convergence.rb +0 -133
- data/examples/24_structured_delegation.rb +0 -151
- data/examples/25_history_search/conversation.jsonl +0 -30
- data/examples/25_history_search.rb +0 -133
- data/examples/26_document_store/api_versioning_adr.md +0 -52
- data/examples/26_document_store/incident_postmortem.md +0 -46
- data/examples/26_document_store/postgres_runbook.md +0 -49
- data/examples/26_document_store/redis_caching_guide.md +0 -48
- data/examples/26_document_store/sidekiq_guide.md +0 -51
- data/examples/26_document_store.rb +0 -140
- data/examples/27_incident_response/README.md +0 -65
- data/examples/27_incident_response/incident_response.rb +0 -261
- data/examples/28_mcp_discovery.rb +0 -113
- data/examples/29_ractor_tools.rb +0 -240
- data/examples/30_ractor_network.rb +0 -252
- data/examples/31_launch_assessment.rb +0 -246
- data/examples/32_newsletter_reader.rb +0 -193
- data/examples/33_stock_generator.rb +0 -80
- data/examples/33_stock_predictor.rb +0 -327
- data/examples/34_agentskills.rb +0 -76
- data/examples/35_hooks.rb +0 -266
- data/examples/README.md +0 -550
- data/examples/common.rb +0 -180
- data/examples/prompts/assistant.md +0 -5
- data/examples/prompts/audit_trail.md +0 -8
- data/examples/prompts/billing.md +0 -10
- data/examples/prompts/classifier.md +0 -7
- data/examples/prompts/comedian.md +0 -6
- data/examples/prompts/comedy_critic.md +0 -10
- data/examples/prompts/configurable.md +0 -9
- data/examples/prompts/dispatcher.md +0 -12
- data/examples/prompts/entity_extractor.md +0 -14
- data/examples/prompts/escalation.md +0 -42
- data/examples/prompts/frontmatter_mcp_test.md +0 -9
- data/examples/prompts/frontmatter_named_test.md +0 -5
- data/examples/prompts/frontmatter_tools_test.md +0 -6
- data/examples/prompts/general.md +0 -7
- data/examples/prompts/github_assistant.md +0 -14
- data/examples/prompts/helper.md +0 -4
- data/examples/prompts/incident_responder.md +0 -18
- data/examples/prompts/keyword_extractor.md +0 -11
- data/examples/prompts/llm_config_demo.md +0 -20
- data/examples/prompts/order_support.md +0 -35
- data/examples/prompts/os_advocate.md +0 -13
- data/examples/prompts/os_chief.md +0 -13
- data/examples/prompts/os_editor.md +0 -13
- data/examples/prompts/parameterized_main_test.md +0 -6
- data/examples/prompts/pii_redactor.md +0 -7
- data/examples/prompts/product_support.md +0 -37
- data/examples/prompts/runbook_protocol.md +0 -12
- data/examples/prompts/sentiment_analyzer.md +0 -12
- data/examples/prompts/skill_a_test.md +0 -4
- data/examples/prompts/skill_b_test.md +0 -4
- data/examples/prompts/skill_config_test.md +0 -5
- data/examples/prompts/skill_cycle_a_test.md +0 -6
- data/examples/prompts/skill_cycle_b_test.md +0 -6
- data/examples/prompts/skill_description_test.md +0 -4
- data/examples/prompts/skill_leaf_test.md +0 -4
- data/examples/prompts/skill_nested_test.md +0 -6
- data/examples/prompts/skill_refs_main_test.md +0 -6
- data/examples/prompts/skill_self_ref_test.md +0 -6
- data/examples/prompts/skill_with_mcp_test.md +0 -9
- data/examples/prompts/skill_with_params_test.md +0 -6
- data/examples/prompts/skill_with_robot_name_test.md +0 -5
- data/examples/prompts/skill_with_tools_test.md +0 -6
- data/examples/prompts/sre_compliance.md +0 -10
- data/examples/prompts/structured_output.md +0 -7
- data/examples/prompts/synthesizer.md +0 -17
- data/examples/prompts/technical.md +0 -10
- data/examples/prompts/template_with_skills_test.md +0 -6
- data/examples/prompts/triage.md +0 -22
- data/examples/ruboruby.md +0 -423
- data/examples/run_all.rb +0 -60
- data/examples/xyzzy.rb +0 -97
- data/lib/robot_lab/mcp/client.rb +0 -223
- data/lib/robot_lab/mcp/connection_poller.rb +0 -195
- data/lib/robot_lab/mcp/transports/base.rb +0 -64
- data/lib/robot_lab/mcp/transports/sse.rb +0 -120
- data/lib/robot_lab/mcp/transports/stdio.rb +0 -170
- data/lib/robot_lab/mcp/transports/streamable_http.rb +0 -141
- data/lib/robot_lab/mcp/transports/websocket.rb +0 -111
- data/logfile +0 -8
- data/mkdocs.yml +0 -209
- data/robot_concurrency.md +0 -38
- data/simple_acp_review.md +0 -298
- data/temp.md +0 -6
- data/tool_manifest_plan.md +0 -155
data/examples/xyzzy.rb
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# xyzzy.rb — single-file RobotLab hook extension demo
|
|
4
|
-
#
|
|
5
|
-
# Demonstrates the Hook handler class pattern: implement class methods for
|
|
6
|
-
# every hook under a dedicated namespace and log each callback with its
|
|
7
|
-
# context snapshot. Useful as a live trace of the full hook pipeline during
|
|
8
|
-
# development.
|
|
9
|
-
#
|
|
10
|
-
# Fully Ractor-safe: no class-level mutable state. LOG_PATH is a frozen
|
|
11
|
-
# string constant (shareable); each call opens its own file handle.
|
|
12
|
-
#
|
|
13
|
-
# stdout : one tagline per hook call → [xyzzy] HH:MM:SS.mmm hook_name
|
|
14
|
-
# logfile: full context snapshot written to LOG_PATH via PP.pp
|
|
15
|
-
#
|
|
16
|
-
# The log destination is read from XYZZY_LOG_PATH at load time and frozen
|
|
17
|
-
# into a constant. Set it BEFORE requiring this file to redirect the log;
|
|
18
|
-
# there is deliberately no writer method, so the constant stays shareable.
|
|
19
|
-
#
|
|
20
|
-
# Usage (from any example that requires common):
|
|
21
|
-
# ENV["XYZZY_LOG_PATH"] = "/tmp/xyzzy.log" # optional, must precede require
|
|
22
|
-
# require_relative "xyzzy"
|
|
23
|
-
|
|
24
|
-
require "pp"
|
|
25
|
-
require "fileutils"
|
|
26
|
-
|
|
27
|
-
module RobotLab
|
|
28
|
-
class Xyzzy < Hook
|
|
29
|
-
self.namespace = :xyzzy
|
|
30
|
-
|
|
31
|
-
LOG_PATH = File.expand_path(
|
|
32
|
-
ENV.fetch("XYZZY_LOG_PATH", "~/.robot_lab/xyzzy_hooks.log")
|
|
33
|
-
).freeze
|
|
34
|
-
FileUtils.mkdir_p(File.dirname(LOG_PATH))
|
|
35
|
-
|
|
36
|
-
class << self
|
|
37
|
-
def before_run(ctx) = log_hook(:before_run, ctx)
|
|
38
|
-
def after_run(ctx) = log_hook(:after_run, ctx)
|
|
39
|
-
def on_error(ctx) = log_hook(:on_error, ctx)
|
|
40
|
-
def before_llm_generation(ctx) = log_hook(:before_llm_generation, ctx)
|
|
41
|
-
def after_llm_generation(ctx) = log_hook(:after_llm_generation, ctx)
|
|
42
|
-
def before_tool_call(ctx) = log_hook(:before_tool_call, ctx)
|
|
43
|
-
def after_tool_call(ctx) = log_hook(:after_tool_call, ctx)
|
|
44
|
-
def before_network_run(ctx) = log_hook(:before_network_run, ctx)
|
|
45
|
-
def after_network_run(ctx) = log_hook(:after_network_run, ctx)
|
|
46
|
-
def before_task(ctx) = log_hook(:before_task, ctx)
|
|
47
|
-
def after_task(ctx) = log_hook(:after_task, ctx)
|
|
48
|
-
|
|
49
|
-
def around_run(ctx, &block)
|
|
50
|
-
log_hook(:around_run, ctx)
|
|
51
|
-
block.call
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def around_llm_generation(ctx, &block)
|
|
55
|
-
log_hook(:around_llm_generation, ctx)
|
|
56
|
-
block.call
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def around_tool_call(ctx, &block)
|
|
60
|
-
log_hook(:around_tool_call, ctx)
|
|
61
|
-
block.call
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def around_network_run(ctx, &block)
|
|
65
|
-
log_hook(:around_network_run, ctx)
|
|
66
|
-
block.call
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def around_task(ctx, &block)
|
|
70
|
-
log_hook(:around_task, ctx)
|
|
71
|
-
block.call
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
private
|
|
75
|
-
|
|
76
|
-
def log_hook(hook_name, ctx)
|
|
77
|
-
ts = Time.now.strftime('%H:%M:%S.%3N')
|
|
78
|
-
snapshot = context_snapshot(ctx)
|
|
79
|
-
$stdout.puts " [xyzzy] #{ts} #{hook_name}"
|
|
80
|
-
File.open(LOG_PATH, "a") { |f| f.puts "#{ts} #{hook_name} | #{PP.pp(snapshot, +"").chomp}" }
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def context_snapshot(ctx)
|
|
84
|
-
{
|
|
85
|
-
robot: ctx.respond_to?(:robot) ? ctx.robot&.name : nil,
|
|
86
|
-
request: ctx.respond_to?(:request) ? ctx.request : nil,
|
|
87
|
-
network: ctx.respond_to?(:network) ? ctx.network&.name : nil,
|
|
88
|
-
task: ctx.respond_to?(:task) ? ctx.task : nil,
|
|
89
|
-
error: ctx.respond_to?(:error) ? ctx.error&.message : nil
|
|
90
|
-
}.compact
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
RobotLab.register_extension(:xyzzy, RobotLab::Xyzzy) if RobotLab.respond_to?(:register_extension)
|
|
97
|
-
RobotLab.on(RobotLab::Xyzzy)
|
data/lib/robot_lab/mcp/client.rb
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module MCP
|
|
5
|
-
# MCP client for communicating with Model Context Protocol servers
|
|
6
|
-
#
|
|
7
|
-
# Uses actionmcp gem for MCP protocol implementation.
|
|
8
|
-
# Supports multiple transport types: StdIO, SSE, WebSocket, HTTP.
|
|
9
|
-
#
|
|
10
|
-
# @example
|
|
11
|
-
# client = Client.new(name: "neon", transport: { type: "ws", url: "ws://..." })
|
|
12
|
-
# client.connect
|
|
13
|
-
# tools = client.list_tools
|
|
14
|
-
# result = client.call_tool("createBranch", { project_id: "abc" })
|
|
15
|
-
#
|
|
16
|
-
# :reek:RepeatedConditional -- @connected is the connection-lifecycle guard; every public operation must check it.
|
|
17
|
-
class Client
|
|
18
|
-
# @!attribute [r] server
|
|
19
|
-
# @return [Server] the MCP server configuration
|
|
20
|
-
# @!attribute [r] connected
|
|
21
|
-
# @return [Boolean] whether currently connected
|
|
22
|
-
attr_reader :server, :connected, :transport
|
|
23
|
-
|
|
24
|
-
# Creates a new MCP Client instance.
|
|
25
|
-
#
|
|
26
|
-
# @param server_or_config [Server, Hash] the server or configuration hash
|
|
27
|
-
# @param poller [MCP::ConnectionPoller, nil] optional shared IO.select poller
|
|
28
|
-
# for multiplexing multiple stdio transports (opt-in, default nil = per-client blocking)
|
|
29
|
-
# @raise [ArgumentError] if config is invalid
|
|
30
|
-
def initialize(server_or_config, poller: nil)
|
|
31
|
-
@server = case server_or_config
|
|
32
|
-
when Server
|
|
33
|
-
server_or_config
|
|
34
|
-
when Hash
|
|
35
|
-
Server.new(**server_or_config.transform_keys(&:to_sym))
|
|
36
|
-
else
|
|
37
|
-
raise ArgumentError, "Invalid server config"
|
|
38
|
-
end
|
|
39
|
-
@connected = false
|
|
40
|
-
@transport = nil
|
|
41
|
-
@request_id = 0
|
|
42
|
-
@poller = poller
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Connect to the MCP server
|
|
46
|
-
#
|
|
47
|
-
# @return [self]
|
|
48
|
-
#
|
|
49
|
-
# :reek:TooManyStatements -- linear connect/register sequence with a best-effort rescue.
|
|
50
|
-
def connect
|
|
51
|
-
return self if @connected
|
|
52
|
-
|
|
53
|
-
@transport = create_transport
|
|
54
|
-
@transport.connect if @transport.respond_to?(:connect)
|
|
55
|
-
@connected = true
|
|
56
|
-
|
|
57
|
-
# Register with shared poller after the transport is connected
|
|
58
|
-
@poller.register(self) if @poller
|
|
59
|
-
|
|
60
|
-
self
|
|
61
|
-
rescue StandardError => e
|
|
62
|
-
RobotLab.config.logger.warn("MCP connection failed for #{@server.name}: #{e.message}")
|
|
63
|
-
@connected = false
|
|
64
|
-
self
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Disconnect from the server
|
|
68
|
-
#
|
|
69
|
-
# @return [self]
|
|
70
|
-
#
|
|
71
|
-
def disconnect
|
|
72
|
-
return self unless @connected
|
|
73
|
-
|
|
74
|
-
@poller.unregister(self) if @poller
|
|
75
|
-
@transport.close if @transport.respond_to?(:close)
|
|
76
|
-
@connected = false
|
|
77
|
-
@transport = nil
|
|
78
|
-
|
|
79
|
-
self
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# List available tools from the server
|
|
83
|
-
#
|
|
84
|
-
# @return [Array<Hash>] Tool definitions
|
|
85
|
-
#
|
|
86
|
-
def list_tools
|
|
87
|
-
ensure_connected!
|
|
88
|
-
response = request(method: "tools/list")
|
|
89
|
-
response[:tools] || []
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Call a tool on the server
|
|
93
|
-
#
|
|
94
|
-
# @param name [String] Tool name
|
|
95
|
-
# @param arguments [Hash] Tool arguments
|
|
96
|
-
# @return [Object] Tool result
|
|
97
|
-
#
|
|
98
|
-
def call_tool(name, arguments = {})
|
|
99
|
-
ensure_connected!
|
|
100
|
-
response = request(
|
|
101
|
-
method: "tools/call",
|
|
102
|
-
params: { name: name, arguments: arguments }
|
|
103
|
-
)
|
|
104
|
-
response[:content] || response
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# List available resources
|
|
108
|
-
#
|
|
109
|
-
# @return [Array<Hash>]
|
|
110
|
-
#
|
|
111
|
-
def list_resources
|
|
112
|
-
ensure_connected!
|
|
113
|
-
response = request(method: "resources/list")
|
|
114
|
-
response[:resources] || []
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Read a resource
|
|
118
|
-
#
|
|
119
|
-
# @param uri [String] Resource URI
|
|
120
|
-
# @return [Object]
|
|
121
|
-
#
|
|
122
|
-
def read_resource(uri)
|
|
123
|
-
ensure_connected!
|
|
124
|
-
response = request(method: "resources/read", params: { uri: uri })
|
|
125
|
-
response[:contents] || response
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# List available prompts
|
|
129
|
-
#
|
|
130
|
-
# @return [Array<Hash>]
|
|
131
|
-
#
|
|
132
|
-
def list_prompts
|
|
133
|
-
ensure_connected!
|
|
134
|
-
response = request(method: "prompts/list")
|
|
135
|
-
response[:prompts] || []
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Get a prompt
|
|
139
|
-
#
|
|
140
|
-
# @param name [String] Prompt name
|
|
141
|
-
# @param arguments [Hash] Prompt arguments
|
|
142
|
-
# @return [Hash]
|
|
143
|
-
#
|
|
144
|
-
def get_prompt(name, arguments = {})
|
|
145
|
-
ensure_connected!
|
|
146
|
-
request(method: "prompts/get", params: { name: name, arguments: arguments })
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Checks if the client is connected to the server.
|
|
150
|
-
#
|
|
151
|
-
# @return [Boolean]
|
|
152
|
-
def connected?
|
|
153
|
-
@connected
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Converts the client to a hash representation.
|
|
157
|
-
#
|
|
158
|
-
# @return [Hash]
|
|
159
|
-
def to_h
|
|
160
|
-
{
|
|
161
|
-
server: @server.to_h,
|
|
162
|
-
connected: @connected
|
|
163
|
-
}
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
private
|
|
167
|
-
|
|
168
|
-
def ensure_connected!
|
|
169
|
-
raise MCPError, "Not connected to MCP server: #{@server.name}" unless @connected
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def create_transport
|
|
173
|
-
config = @server.transport.merge(timeout: @server.timeout)
|
|
174
|
-
|
|
175
|
-
case @server.transport_type
|
|
176
|
-
when "stdio"
|
|
177
|
-
Transports::Stdio.new(config)
|
|
178
|
-
when "ws", "websocket"
|
|
179
|
-
Transports::WebSocket.new(config)
|
|
180
|
-
when "sse"
|
|
181
|
-
Transports::SSE.new(config)
|
|
182
|
-
when "streamable-http", "http"
|
|
183
|
-
Transports::StreamableHTTP.new(config)
|
|
184
|
-
else
|
|
185
|
-
raise MCPError, "Unsupported transport type: #{@server.transport_type}"
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def request(method:, params: nil)
|
|
190
|
-
@request_id += 1
|
|
191
|
-
|
|
192
|
-
message = {
|
|
193
|
-
jsonrpc: "2.0",
|
|
194
|
-
id: @request_id,
|
|
195
|
-
method: method
|
|
196
|
-
}
|
|
197
|
-
message[:params] = params if params
|
|
198
|
-
|
|
199
|
-
response = if @poller && @transport.is_a?(Transports::Stdio)
|
|
200
|
-
@poller.send_request(self, message, timeout: @server.timeout)
|
|
201
|
-
else
|
|
202
|
-
@transport.send_request(message)
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
parse_response(response)
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
def parse_response(response)
|
|
209
|
-
case response
|
|
210
|
-
when String
|
|
211
|
-
parsed = JSON.parse(response, symbolize_names: true)
|
|
212
|
-
parsed[:result] || parsed
|
|
213
|
-
when Hash
|
|
214
|
-
response[:result] || response
|
|
215
|
-
else
|
|
216
|
-
response
|
|
217
|
-
end
|
|
218
|
-
rescue JSON::ParserError
|
|
219
|
-
{ raw: response }
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
end
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module MCP
|
|
5
|
-
# Multiplexes I/O across multiple stdio MCP transports.
|
|
6
|
-
#
|
|
7
|
-
# When a robot connects to more than one local (stdio) MCP server,
|
|
8
|
-
# the default approach blocks independently per client, each with its
|
|
9
|
-
# own Timeout.timeout wrapper. ConnectionPoller replaces this with a
|
|
10
|
-
# single IO.select call across all registered stdout file descriptors,
|
|
11
|
-
# dispatching each response to the pending request for that client.
|
|
12
|
-
#
|
|
13
|
-
# Async-based transports (SSE, WebSocket, StreamableHTTP) are
|
|
14
|
-
# unaffected — they already use the Async fiber scheduler.
|
|
15
|
-
#
|
|
16
|
-
# == Usage
|
|
17
|
-
#
|
|
18
|
-
# poller = MCP::ConnectionPoller.new.start
|
|
19
|
-
# client = MCP::Client.new(server_config, poller: poller)
|
|
20
|
-
# client.connect # registers transport with poller
|
|
21
|
-
# client.call_tool(…)
|
|
22
|
-
# poller.stop
|
|
23
|
-
#
|
|
24
|
-
# @api private
|
|
25
|
-
class ConnectionPoller
|
|
26
|
-
POLL_INTERVAL = 0.1 # seconds between IO.select checks
|
|
27
|
-
|
|
28
|
-
# Creates a new ConnectionPoller.
|
|
29
|
-
def initialize
|
|
30
|
-
@mutex = Mutex.new
|
|
31
|
-
@clients = {} # IO => { client:, queue: Thread::Queue }
|
|
32
|
-
@running = false
|
|
33
|
-
@thread = nil
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Start the multiplexing thread.
|
|
37
|
-
#
|
|
38
|
-
# @return [self]
|
|
39
|
-
def start
|
|
40
|
-
@mutex.synchronize do
|
|
41
|
-
return self if @running
|
|
42
|
-
|
|
43
|
-
@running = true
|
|
44
|
-
@thread = Thread.new { poll_loop }
|
|
45
|
-
@thread.name = "RobotLab::MCP::ConnectionPoller"
|
|
46
|
-
end
|
|
47
|
-
self
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Stop the multiplexing thread.
|
|
51
|
-
#
|
|
52
|
-
# Cancels all pending requests with an MCPError.
|
|
53
|
-
#
|
|
54
|
-
# @param timeout [Numeric] seconds to wait for thread to finish
|
|
55
|
-
# @return [self]
|
|
56
|
-
def stop(timeout: 5)
|
|
57
|
-
@mutex.synchronize do
|
|
58
|
-
return self unless @running
|
|
59
|
-
|
|
60
|
-
@running = false
|
|
61
|
-
|
|
62
|
-
# Unblock any pending request queues
|
|
63
|
-
@clients.each_value do |entry|
|
|
64
|
-
entry[:queue]&.push({ error: "ConnectionPoller stopped" })
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
@thread&.join(timeout)
|
|
69
|
-
@thread = nil
|
|
70
|
-
self
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Register an MCP client with the poller.
|
|
74
|
-
#
|
|
75
|
-
# Only stdio clients are registered — others are silently ignored.
|
|
76
|
-
#
|
|
77
|
-
# @param client [MCP::Client]
|
|
78
|
-
# @return [void]
|
|
79
|
-
def register(client)
|
|
80
|
-
return unless stdio_client?(client)
|
|
81
|
-
|
|
82
|
-
io = client.transport.stdout
|
|
83
|
-
@mutex.synchronize { @clients[io] = { client: client, queue: nil } }
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Unregister a client.
|
|
87
|
-
#
|
|
88
|
-
# @param client [MCP::Client]
|
|
89
|
-
# @return [void]
|
|
90
|
-
def unregister(client)
|
|
91
|
-
return unless stdio_client?(client)
|
|
92
|
-
|
|
93
|
-
io = client.transport.stdout
|
|
94
|
-
@mutex.synchronize { @clients.delete(io) }
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Send a JSON-RPC request via the poller and block until the response.
|
|
98
|
-
#
|
|
99
|
-
# Writes to the client's stdin, registers a response queue, then
|
|
100
|
-
# blocks until the poll loop dispatches the response.
|
|
101
|
-
#
|
|
102
|
-
# @param client [MCP::Client]
|
|
103
|
-
# @param message [Hash] JSON-RPC message
|
|
104
|
-
# @param timeout [Numeric] seconds before raising MCPError
|
|
105
|
-
# @return [Hash] parsed response
|
|
106
|
-
# @raise [MCPError] on timeout or connection error
|
|
107
|
-
# :reek:TooManyStatements -- register/write/wait/cleanup must stay in one method so ensure releases the queue.
|
|
108
|
-
# :reek:DuplicateMethodCall -- the repeated @mutex.synchronize/@clients[io] pairs are deliberately separate short critical
|
|
109
|
-
# sections; holding the lock across the blocking write/wait would deadlock the poll loop.
|
|
110
|
-
def send_request(client, message, timeout:)
|
|
111
|
-
transport = client.transport
|
|
112
|
-
io = transport.stdout
|
|
113
|
-
stdin = transport.stdin
|
|
114
|
-
queue = Thread::Queue.new
|
|
115
|
-
|
|
116
|
-
@mutex.synchronize { @clients[io][:queue] = queue }
|
|
117
|
-
|
|
118
|
-
begin
|
|
119
|
-
stdin.puts(message.to_json)
|
|
120
|
-
stdin.flush
|
|
121
|
-
rescue Errno::EPIPE, IOError => e
|
|
122
|
-
@mutex.synchronize { @clients[io][:queue] = nil }
|
|
123
|
-
raise MCPError.new("MCP connection lost: #{e.message}", retryable: true)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
response = Timeout.timeout(timeout) { queue.pop }
|
|
127
|
-
|
|
128
|
-
if response.is_a?(Hash) && response[:error]
|
|
129
|
-
raise MCPError, response[:error]
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
response
|
|
133
|
-
rescue Timeout::Error
|
|
134
|
-
@mutex.synchronize { @clients[io]&.[]= :queue, nil }
|
|
135
|
-
raise MCPError.new("MCP server did not respond within #{timeout}s", retryable: true)
|
|
136
|
-
ensure
|
|
137
|
-
@mutex.synchronize { @clients[io]&.[]= :queue, nil }
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# Whether the poller thread is running.
|
|
141
|
-
#
|
|
142
|
-
# @return [Boolean]
|
|
143
|
-
def running?
|
|
144
|
-
@mutex.synchronize { @running }
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
private
|
|
148
|
-
|
|
149
|
-
def poll_loop
|
|
150
|
-
loop do
|
|
151
|
-
ios = @mutex.synchronize { @clients.keys.reject(&:closed?) }
|
|
152
|
-
|
|
153
|
-
if ios.empty?
|
|
154
|
-
sleep POLL_INTERVAL
|
|
155
|
-
else
|
|
156
|
-
begin
|
|
157
|
-
ready, = IO.select(ios, nil, nil, POLL_INTERVAL)
|
|
158
|
-
dispatch(ready) if ready
|
|
159
|
-
rescue Errno::EBADF
|
|
160
|
-
# A pipe was closed between the reject and IO.select — harmless, loop again
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
break unless @mutex.synchronize { @running }
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
# :reek:TooManyStatements -- read/parse/filter/route steps for each readable IO; each guard skips a non-response line.
|
|
169
|
-
def dispatch(readable_ios)
|
|
170
|
-
readable_ios.each do |io|
|
|
171
|
-
line = io.gets rescue nil
|
|
172
|
-
next unless line
|
|
173
|
-
|
|
174
|
-
parsed = JSON.parse(line, symbolize_names: true) rescue nil
|
|
175
|
-
next unless parsed
|
|
176
|
-
|
|
177
|
-
# Skip notifications (method present, no id)
|
|
178
|
-
next if parsed[:method] && !parsed.key?(:id)
|
|
179
|
-
|
|
180
|
-
entry = @mutex.synchronize { @clients[io] }
|
|
181
|
-
entry[:queue]&.push(parsed)
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def stdio_client?(client)
|
|
186
|
-
return false unless client.respond_to?(:transport)
|
|
187
|
-
|
|
188
|
-
transport = client.transport
|
|
189
|
-
transport.is_a?(Transports::Stdio) &&
|
|
190
|
-
transport.respond_to?(:stdout) &&
|
|
191
|
-
!transport.stdout.nil?
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
end
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module MCP
|
|
5
|
-
module Transports
|
|
6
|
-
# Base class for MCP transports
|
|
7
|
-
#
|
|
8
|
-
# @abstract Subclass and implement {#connect}, {#send_request}, {#close}
|
|
9
|
-
#
|
|
10
|
-
class Base
|
|
11
|
-
# Default timeout for request operations (in seconds)
|
|
12
|
-
DEFAULT_TIMEOUT = 15
|
|
13
|
-
|
|
14
|
-
# @!attribute [r] config
|
|
15
|
-
# @return [Hash] the transport configuration
|
|
16
|
-
# @!attribute [r] timeout
|
|
17
|
-
# @return [Numeric] request timeout in seconds
|
|
18
|
-
attr_reader :config, :timeout
|
|
19
|
-
|
|
20
|
-
# Creates a new transport instance.
|
|
21
|
-
#
|
|
22
|
-
# @param config [Hash] transport configuration options.
|
|
23
|
-
# May include a :timeout key (in seconds) that controls how long
|
|
24
|
-
# blocking operations wait before raising an error.
|
|
25
|
-
def initialize(config)
|
|
26
|
-
@config = config.transform_keys(&:to_sym)
|
|
27
|
-
@timeout = @config.delete(:timeout) || DEFAULT_TIMEOUT
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Connect to the server
|
|
31
|
-
#
|
|
32
|
-
# @return [self]
|
|
33
|
-
#
|
|
34
|
-
def connect
|
|
35
|
-
raise NotImplementedError
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Send a JSON-RPC request
|
|
39
|
-
#
|
|
40
|
-
# @param message [Hash] JSON-RPC message
|
|
41
|
-
# @return [Hash] Response
|
|
42
|
-
#
|
|
43
|
-
def send_request(message)
|
|
44
|
-
raise NotImplementedError
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Close the connection
|
|
48
|
-
#
|
|
49
|
-
# @return [self]
|
|
50
|
-
#
|
|
51
|
-
def close
|
|
52
|
-
raise NotImplementedError
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Check if the transport is connected.
|
|
56
|
-
#
|
|
57
|
-
# @return [Boolean] true if connected
|
|
58
|
-
def connected?
|
|
59
|
-
false
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module MCP
|
|
5
|
-
module Transports
|
|
6
|
-
# Server-Sent Events transport for MCP servers
|
|
7
|
-
#
|
|
8
|
-
# Uses async-http for SSE streaming.
|
|
9
|
-
#
|
|
10
|
-
# @example
|
|
11
|
-
# transport = SSE.new(url: "http://localhost:8080/sse")
|
|
12
|
-
#
|
|
13
|
-
# :reek:InstanceVariableAssumption -- @config is assigned in Base#initialize (reek does not trace super).
|
|
14
|
-
# :reek:RepeatedConditional -- @connected is the connection-lifecycle guard; every operation must check it.
|
|
15
|
-
class SSE < Base
|
|
16
|
-
# Creates a new SSE transport.
|
|
17
|
-
#
|
|
18
|
-
# @param config [Hash] transport configuration
|
|
19
|
-
# @option config [String] :url SSE server URL
|
|
20
|
-
def initialize(config)
|
|
21
|
-
super
|
|
22
|
-
@client = nil
|
|
23
|
-
@connected = false
|
|
24
|
-
@event_queue = []
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Connect to the MCP server via SSE.
|
|
28
|
-
#
|
|
29
|
-
# @return [self]
|
|
30
|
-
# @raise [MCPError] if async-http gem is not available
|
|
31
|
-
# :reek:TooManyStatements -- linear require/connect/handshake sequence inside the Async block.
|
|
32
|
-
def connect
|
|
33
|
-
return self if @connected
|
|
34
|
-
|
|
35
|
-
require "async"
|
|
36
|
-
require "async/http/client"
|
|
37
|
-
require "async/http/endpoint"
|
|
38
|
-
|
|
39
|
-
url = @config[:url]
|
|
40
|
-
|
|
41
|
-
Async do
|
|
42
|
-
endpoint = Async::HTTP::Endpoint.parse(url)
|
|
43
|
-
@client = Async::HTTP::Client.new(endpoint)
|
|
44
|
-
@connected = true
|
|
45
|
-
|
|
46
|
-
# Initialize MCP protocol
|
|
47
|
-
send_initialize
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
self
|
|
51
|
-
rescue LoadError => e
|
|
52
|
-
raise MCPError, "async-http gem required for SSE transport: #{e.message}"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Send a JSON-RPC request to the MCP server.
|
|
56
|
-
#
|
|
57
|
-
# @param message [Hash] JSON-RPC message
|
|
58
|
-
# @return [Hash] the response
|
|
59
|
-
# @raise [MCPError] if not connected
|
|
60
|
-
def send_request(message)
|
|
61
|
-
raise MCPError, "Not connected" unless @connected
|
|
62
|
-
|
|
63
|
-
require "async"
|
|
64
|
-
require "async/http/body/writable"
|
|
65
|
-
|
|
66
|
-
Async do
|
|
67
|
-
# POST the request
|
|
68
|
-
response = @client.post(
|
|
69
|
-
@config[:url],
|
|
70
|
-
{ "Content-Type" => "application/json" },
|
|
71
|
-
[message.to_json]
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
# Read response
|
|
75
|
-
body = response.read
|
|
76
|
-
JSON.parse(body, symbolize_names: true)
|
|
77
|
-
end.wait
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Close the SSE connection.
|
|
81
|
-
#
|
|
82
|
-
# @return [self]
|
|
83
|
-
def close
|
|
84
|
-
return self unless @connected
|
|
85
|
-
|
|
86
|
-
@client&.close
|
|
87
|
-
@connected = false
|
|
88
|
-
@client = nil
|
|
89
|
-
|
|
90
|
-
self
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Check if the transport is connected.
|
|
94
|
-
#
|
|
95
|
-
# @return [Boolean] true if connected
|
|
96
|
-
def connected?
|
|
97
|
-
@connected
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
private
|
|
101
|
-
|
|
102
|
-
def send_initialize
|
|
103
|
-
send_request(
|
|
104
|
-
jsonrpc: "2.0",
|
|
105
|
-
id: 0,
|
|
106
|
-
method: "initialize",
|
|
107
|
-
params: {
|
|
108
|
-
protocolVersion: "2024-11-05",
|
|
109
|
-
capabilities: {},
|
|
110
|
-
clientInfo: {
|
|
111
|
-
name: "RobotLab",
|
|
112
|
-
version: RobotLab::VERSION
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|