Skip to content
Get started
Build Something

Build Something

An end-to-end guide of building an agent and MCP server in Ruby

There are few things we need to make clear right off of the bat:

  1. Cadenya is not an Agent SDK that runs the agent loop in your infrastructure.
  2. Cadenya makes network calls to your API/MCP server for you
  3. Cadenya is the agentic loop.

You’re an engineer that has been building and maintaining a product that has network addressable APIs, MCPs, etc. You work for a company (or are building one yourself) that wants to connect the functionalities of your product to LLMs to build the next autonomous product. You’ve looked at Agent SDKs, LangChain, CrewAI, and realized you’re still the one hosting new infrastructure. You dislike that. You are here now reading this handwritten guide. Lucky you.

  • Hosting your agent’s behavior (ie: tools). This can be an MCP server, or API. Dealers choice. It can also be hundreds of different MCP servers.
  • You’re responsible for initializing objective’s in Cadenya using the Cadenya API (which has SDKs in several languages). If you are analyzing a receipt or responding to a support ticket - that’s an objective in our lingo.

Alt heading: What Cadenya does for you

  1. Providing the agentic loop AI Agents require
  2. Retrying tools on transient failures, approval management, and memory management
  3. Reliability and Scaleability
  4. Context management (and compaction, built in)
  5. Agent feedback and natural selection process
  6. Tracking tool usage, agent success, and total usage of your agents.
  • Let’s use a very simple MCP app for an agent in Ruby (because it’s easy to read)
  • ngrok for a public endpoint
  • Cadenya for connecting them an LLM

Let’s create a new directory w/ a single Ruby file in it

Terminal window
mkdir -p cadenya-ruby-mcp-example
cd cadenya-ruby-mcp-example
touch app.rb

In our shiny new file, let’s add the code:

app.rb
require 'bundler/inline'
require 'tzinfo'
gemfile do
source 'https://rubygems.org'
# Uses the official MCP Rubygem
gem 'mcp'
# Server specific to run the MCP server
gem 'rack'
gem 'puma'
gem 'tzinfo'
end
# Declare
class WhatTimeIsIt < MCP::Tool
description "Gets the time in a particular timezone"
input_schema(
properties: { timezone: { type: "string" } },
required: ["timezone"]
)
class << self
def call(timezone:, server_context:)
zone = TZInfo::Timezone.get(timezone)
time = zone.now
MCP::Tool::Response.new([{
type: "text",
text: "The time is: #{time.iso8601} for the Timezone '#{zone.to_s}'",
}])
end
end
end
server = MCP::Server.new(
name: "timeserver",
tools: [WhatTimeIsIt],
)
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
port = (ENV['PORT'] || 3000).to_i
host = (ENV['HOST'] || "127.0.0.1")
puts "running mcp server on #{host}:#{port}"
Puma::Server.new(transport).tap { |s| s.add_tcp_listener(host, port) }.run.join

And make sure that is saved. Then, assuming you have ruby installed on your machine:

Terminal window
ruby app.rb

You should see in a log line for the host and port.

Check to see if you have ngrok installed first:

Terminal window
which ngrok

If an error is printed, you don’t have ngrok, and you need to register/install it. before moving on.

Open a new tab or terminal window. Then, using ngrok, you can expose this new MCP server to the world. Or more importantly: Cadenya..

Terminal window
ngrok http 3000

At this point this is what you should have:

  1. A running ruby process that is serving that MCP server in a terminal tab/window.
  2. A second ngrok process running exposing port 3000 to the open internet.

This is where Cadenya can now step in and connect an LLM to your MCP endpoint. We’re going to leverage Cadenya’s bulk resource API to quickly configure this app.

Let’s create a new file to store our config for our example:

Terminal window
touch cadenya.yaml

Cadenya supports being configured by a YAML file. The structure matches the API Endpoint for Bulk Resources. Whatever your ngrok domain is after starting it, replace in the {ngrok_url} portion of the example file below.

cadenya.yaml
data:
bundleKey: "ruby-mcp-example"
toolSets:
ruby:
name: "Ruby MCP Example"
spec:
adapter:
mcp:
url: "{ngrok_url}"
agents:
timezoner:
name: "Timezoner"
spec:
status: AGENT_STATUS_PUBLISHED
variations:
default:
name: "Default Variation"
spec:
modelId: "external_id:anthropic-claude-sonnet-latest"
assignments:
- toolSetId: "external_id:ruby"

The cadenya.yaml file is designed to pair with the Cadenya CLI. To install it via Homebrew:

Terminal window
brew tap cadenya/tools
brew install cadenya-cli

Alternatively, you can install with Go install:

Terminal window
go install 'github.com/cadenya/cadenya-cli/cmd/cadenya@latest'

Setting your environment variable for CADENYA_API_KEY is the easiest way to access the Cadenya API with the CLI. Read this guide to learn where to grab it. (Hint: It’s in the bottom left when you’re logged in). Once you grab your API key, you can set it in your terminal for the rest of the commands we’ll run:

Terminal window
export CADENYA_API_KEY={your_api_key}

Once you have Cadenya CLI installed, you can load the cadenya.yaml. But first you’ll need the workspace ID for your Cadenya account.

Terminal window
cadenya workspaces list --format=yaml

You should see something like this in your terminal:

metadata:
id: workspace_01KQXM7Z5WCK7DX2N43ERVW4YB
accountId: account_01KQXM7XJWSPPTRETZNQTJ3MHB
name: SYS
profileId: ""
externalId: ""
labels: null
spec:
description: ""

And, we can quickly assign the workspace ID to an environment variable using jq

Terminal window
export WORKSPACE_ID=$(cadenya workspaces list --format=raw | jq -r '.items[0].metadata.id')

From here, we can submit our cadenya.yaml we created earlier to Cadenya to process. Cadenya will resolve foreign keys, do a diff on the submitted value, and update your resources.

Terminal window
cadenya bulk-workspace-resources apply --workspace-id $WORKSPACE_ID --format=pretty < cadenya.yaml

When the resources are submitted, Cadenya will apply the configuration asynchronously. Tool sets, agents, and anytning else you include in your cadenya.yaml file will be created (or updated) quickly, but not instantly.

Hopefully, by the time you read that sentence, the sync is completed, and you can run this command:

Terminal window
cadenya tool-sets:tools list --workspace-id $WORKSPACE_ID --tool-set-id "external_id:ruby" --format=pretty

If the tool set is created, and Cadenya has access to your running ngrok/ruby MCP server, you should see a single tool that has synced in the response.

One thing you might notice about the flags, that awesome external_id:ruby. Cadenya’s API allows you to define your own externalId on any resource or operation on creation. And anytime you see a foreign key identifier parameter (either in URL paths, or resource specifications), will accept an external ID you’ve defined.

You might be saying “Cool trick, but now what?” – harsh but fair. Let’s connect the final dot: Objectives.

Agents and their variations are static in nature. Objectives are what animate agents in Cadenya. We can kickoff an objective using a similar command as before:

Terminal window
cadenya objectives create \
--workspace-id $WORKSPACE_ID \
--agent-id "external_id:timezoner" \
--data '{initialMessage: "What time is it in Tokyo?"}' \
--format pretty

This command is using the same externalId trick as before, where timezoner is the external ID from our cadenya.yaml (the hashes key for the defined agent).

You’ll see a new objective operation created.

Terminal window
# Output:
#
# metadata:
# id: obj_01KR5640KA07FYZC9JQT74VN3S
# accountId: account_01KQXM7XJWSPPTRETZNQTJ3MHB
# workspaceId: workspace_01KQXM7Z5WCK7DX2N43ERVW4YB
# createdAt: "2026-05-09T01:37:35.850248856Z"
# profileId: profile_01KQXM7ZW6665REFASQ8H8YSQ2
# data:
# agent:
# metadata:
# id: agent_01KR4M6WBFFR16ZZZY2GX55T89
# initialMessage: What time is it in Tokyo?

Once the objective is created, your agent should spring into action, load the tools given to it (like your MCP server), and start an agentic loop. Because the prompt from the example is so explicit (What time is it in Tokyo?), the agent will call the tool for grabbing the time in that timezone.

Terminal window
cadenya objectives:tool-calls list \
--workspace-id $WORKSPACE_ID \
--objective-id obj_01KR554FRJQF17MXYC9198WMZM \
--format json | jq .data.callable.tool
# {
# "id": "tool_01KR4M6X4PKEPGFEJ3VCREYKS8",
# "accountId": "",
# "createdAt": "0001-01-01T00:00:00Z",
# "name": "what_time_is_it",
# "profileId": "",
# "workspaceId": "",
# "bundleKey": "",
# "externalId": "",
# "labels": null
# }

You can login to app.cadenya.com and visit the Objectives page to see your new objective that we created created. Visit the agents, tool sets, etc, to see the resources we created in this tutorial. For convience, you can run this in your terminal:

Terminal window
open "https://app.cadenya.com/w/$WORKSPACE_ID/objectives"

Cadenya’s goal is to connect inference to outcomes. With this guide, you’ve created a simple MCP server using Ruby, and connected that behavior to an LLM that can assign tools dynamically, manage compaction, and give you analytics about everything that is happening with your agents.

The power of Cadenya is felt when your agents have access to more tools. Agents can dozens of tool sets given to them, and will conquer complex tasks using them.