Build Something
An end-to-end guide of building an agent and MCP server in Ruby
First things first
Section titled “First things first”There are few things we need to make clear right off of the bat:
- Cadenya is not an Agent SDK that runs the agent loop in your infrastructure.
- Cadenya makes network calls to your API/MCP server for you
- Cadenya is the agentic loop.
Who you are (yes, you!)
Section titled “Who you are (yes, you!)”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.
What you are responsible for
Section titled “What you are responsible for”- 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.
What you are not responsible for anymore
Section titled “What you are not responsible for anymore”Alt heading: What Cadenya does for you
- Providing the agentic loop AI Agents require
- Retrying tools on transient failures, approval management, and memory management
- Reliability and Scaleability
- Context management (and compaction, built in)
- Agent feedback and natural selection process
- Tracking tool usage, agent success, and total usage of your agents.
Show me code
Section titled “Show me code”- 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
MCP Server
Section titled “MCP Server”Let’s create a new directory w/ a single Ruby file in it
mkdir -p cadenya-ruby-mcp-examplecd cadenya-ruby-mcp-exampletouch app.rbIn our shiny new file, let’s add the code:
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
# Declareclass 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 endend
server = MCP::Server.new( name: "timeserver", tools: [WhatTimeIsIt],)
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
port = (ENV['PORT'] || 3000).to_ihost = (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.joinAnd make sure that is saved. Then, assuming you have ruby installed on your machine:
ruby app.rbYou should see in a log line for the host and port.
Ngrok Time
Section titled “Ngrok Time”Check to see if you have ngrok installed first:
which ngrokIf 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..
ngrok http 3000At this point this is what you should have:
- A running ruby process that is serving that MCP server in a terminal tab/window.
- A second
ngrokprocess running exposing port 3000 to the open internet.
Where Cadenya Steps In
Section titled “Where Cadenya Steps In”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.
cadenya.yaml
Section titled “cadenya.yaml”Let’s create a new file to store our config for our example:
touch cadenya.yamlCadenya 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.
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"Installing the Cadenya CLI
Section titled “Installing the Cadenya CLI”The cadenya.yaml file is designed to pair with the Cadenya CLI. To install it via Homebrew:
brew tap cadenya/toolsbrew install cadenya-cliAlternatively, you can install with Go install:
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:
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.
cadenya workspaces list --format=yamlYou should see something like this in your terminal:
metadata: id: workspace_01KQXM7Z5WCK7DX2N43ERVW4YB accountId: account_01KQXM7XJWSPPTRETZNQTJ3MHB name: SYS profileId: "" externalId: "" labels: nullspec: description: ""And, we can quickly assign the workspace ID to an environment variable using jq
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.
cadenya bulk-workspace-resources apply --workspace-id $WORKSPACE_ID --format=pretty < cadenya.yamlWhen 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:
cadenya tool-sets:tools list --workspace-id $WORKSPACE_ID --tool-set-id "external_id:ruby" --format=prettyIf 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.
Now what?
Section titled “Now what?”You might be saying “Cool trick, but now what?” – harsh but fair. Let’s connect the final dot: Objectives.
Kicking off an objective
Section titled “Kicking off an objective”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:
cadenya objectives create \ --workspace-id $WORKSPACE_ID \ --agent-id "external_id:timezoner" \ --data '{initialMessage: "What time is it in Tokyo?"}' \ --format prettyThis 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.
# 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.
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# }See it for yourself
Section titled “See it for yourself”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:
open "https://app.cadenya.com/w/$WORKSPACE_ID/objectives"What now?
Section titled “What now?”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.