--- title: Build Something | cadenya description: An end-to-end guide of building an agent and MCP server in Ruby --- New to Cadenya? Read the [Getting Started](/guides/getting-started/index.md) guide first to understand how the pieces fit together. Or jump in below, your prerogative. ## First things first 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.* ## 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 - 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 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. ## 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 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. ### Ngrok Time 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.](https://ngrok.com/) 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. ### 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` 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](/api/resources/bulk_workspace_resources/methods/apply/index.md). 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" ``` ### Installing the Cadenya CLI 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](/guides/api-design/index.md). (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. ## 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 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 # } ``` ## See it for yourself You can login to [app.cadenya.com](https://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" ``` ## 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.