Building AI Agents in Ruby with the Anthropic SDK The official Anthropic Ruby SDK now supports building AI agents in Rails with tool design, MCP servers, human approval gating, testing, and cost control. The SDK includes streaming, connection pooling, and a tool runner that handles the agent loop, enabling developers to create agents that dynamically use tools to accomplish open-ended tasks. Anthropic advises using the simplest solution possible, often a single model call, before resorting to full agent loops. Building AI Agents in Ruby with the Anthropic SDK Build a Rails AI agent safe to ship: tool design, MCP servers, human approval gating, testing, and cost control using the official Anthropic Ruby SDK in Rails. An AI agent is a language model that actually does things. You hand it a goal and a set of tools functions it is allowed to call , and it decides which to use, runs them, reads the results, and keeps going until the task is done. That loop of deciding, acting, and observing is what separates an agent from a single prompt. A support agent that looks up customer invoices and drafts a reply, or an internal tool that pulls from three systems to answer a question, is an agent in this sense. The official Anthropic Ruby SDK https://github.com/anthropics/anthropic-sdk-ruby ships with streaming, connection pooling, and a tool runner that handles the agent loop for you. This post covers what an agent actually is, how to structure one in Rails, how to design tools the model can use reliably, and the production concerns that make the difference between a demo and something you can actually ship. What an Agent Actually Is The concept is simple. In Anthropic's words, "agents are typically just LLMs using tools based on environmental feedback in a loop" Building Effective Agents https://www.anthropic.com/engineering/building-effective-agents . The model receives a goal, decides whether it needs to call a tool, you execute the tool and feed the result back, and the loop repeats until the model stops asking for tools. The same article draws a distinction worth understanding before you write any code. "Workflows are systems where LLMs and tools are orchestrated through predefined code paths," while "agents are systems where LLMs dynamically direct their own processes and tool usage, maintaining control over how they accomplish tasks." Workflows are predictable and consistent; agents are flexible at the cost of higher latency, higher token spend, and the potential for compounding errors. Which of these you actually need is the call that matters here, and the honest answer is usually "less agent than you think." Anthropic's own guidance is to find "the simplest solution possible, and only increasing complexity when needed." For many features, a single well-prompted model call with good context beats an autonomous agent: cheaper, faster, and easier to debug. Reach for a true loop only when the task is open-ended enough that you genuinely cannot predict the steps in advance. The Minimal Agent Loop in Ruby Start with the official gem: Gemfile gem "anthropic" The client is threadsafe and maintains its own connection pool, so create it once and reuse it. An initializer is the natural home: config/initializers/anthropic.rb ANTHROPIC = Anthropic::Client.new api key: ENV.fetch "ANTHROPIC API KEY" A single model call looks like this: message = ANTHROPIC.messages.create model: "claude-sonnet-4-6", max tokens: 1024, messages: { role: "user", content: "Summarize Q1 in one sentence." } puts message.content That is not yet an agent, because there is no loop and no tools. The loop is what makes it agentic: send the conversation to the model, check whether it wants to use a tool, run the tool, append the result to the conversation, and repeat until it stops asking for tools. Written by hand, the loop is only a dozen lines, and it is worth seeing once before you let the SDK handle it, because understanding what is under the abstraction is what lets you debug it when it breaks. python def run agent client:, tools:, messages:, model: "claude-sonnet-4-6" loop do response = client.messages.create model: model, max tokens: 1024, tools: tools.map &:definition , messages: messages The model is done when it stops asking to use tools. break response if response.stop reason = "tool use" messages << { role: "assistant", content: response.content } tool results = response.content .select { |block| block.type == "tool use" } .map { |block| execute tool tools, block } messages << { role: "user", content: tool results } end end This is the core of every agent. Everything else is refinement: better tools, streaming, error handling, observability, and guardrails. The model drives, your code executes the tools, and each result feeds back so the model can judge its own progress. Designing Tools the Model Can Actually Use You will spend more time on your tools than on your prompts. When Anthropic built their own coding agent, they spent more time optimizing the tools than the overall prompt. A tool definition is an interface, and the model is the consumer of that interface. A confusing tool produces a confused agent. Anthropic frames this as the agent-computer interface, or ACI: "Think about how much effort goes into human-computer interfaces HCI , and plan to invest just as much effort in creating good agent-computer interfaces ACI ." A tool definition should read like a docstring written for a competent new engineer with no other context: what it does, when to use it, what each parameter means, and where the edges are. The official SDK lets you define tools as Ruby classes with a typed input schema: class LookupInvoicesInput < Anthropic::BaseModel required :customer id, Integer optional :status, Anthropic::InputSchema::EnumOf :draft, :open, :paid, :overdue optional :limit, Integer end class LookupInvoices < Anthropic::BaseTool description <<~TEXT Look up invoices for a single customer. Use this when the user asks about a specific customer's billing, outstanding balance, or payment history. Returns at most limit invoices default 20 , newest first. Does not search across customers; call once per customer. TEXT input schema LookupInvoicesInput def call input scope = Invoice.where customer id: input.customer id scope = scope.where status: input.status if input.status scope.order created at: :desc .limit input.limit || 20 .as json only: %i id number status amount cents due on end end Several choices here are deliberate. The description tells the model when to use the tool, not just what it does, and it explicitly states a boundary "does not search across customers" . Models make mistakes at exactly these boundaries, so naming them in the description prevents whole classes of error. When Anthropic switched a tool to require absolute file paths rather than relative ones, it eliminated a recurring model mistake. The input schema is typed and uses an enum for status, which means the model cannot invent a status value your code does not handle. Constrain the inputs so it is hard to make a mistake. The return value is a deliberately narrow projection, not the full ActiveRecord object. Every field you return is tokens the model has to read and you have to pay for. Returning columns the task does not need is pure waste, and your database rows often contain fields you do not want in the model's context at all. A good rule: start with a few thoughtful tools targeting specific high-impact tasks, not a sprawling library of thin wrappers around every endpoint you have. A few tools that compose well beat a pile of overlapping ones. Writing a Good System Prompt The system prompt is the one piece of configuration that shapes the entire agent experience. It should tell the model who it is, what it is for, what it should never do, and how it should present itself to the user. A minimal system prompt for a support agent might look like this: SYSTEM PROMPT = <<~PROMPT You are a billing support assistant for Acme SaaS. You help users understand their invoices, payment history, and subscription status. You have access to tools that can look up invoices and subscription data. Always verify the customer's identity before discussing account details. Be friendly and concise. Use markdown formatting and emojis to make responses scannable and approachable. Inject occasional warmth and humor where it fits naturally. After answering, suggest 2-3 follow-up questions the user might find useful, phrased as clickable options. Do not discuss competitors, pricing negotiations, or refunds above $500. Escalate those to a human agent instead. PROMPT A few things worth calling out here. Provide context, not just rules. The model performs better when it understands the purpose behind the constraints. "Do not discuss refunds above $500 escalate to a human agent " explains the rule and what to do instead. "Do not discuss refunds above $500" leaves the model to guess what happens next. Be specific about escalation paths. Vague "do not do harmful things" instructions are much weaker than concrete rules with explicit fallbacks. Name the exact scenarios and tell the model exactly what to do in each one. The system prompt is also where you decide how the agent presents itself, which is worth treating as a feature in its own right. Make the Agent Feel Human An agent that gives correct answers in a flat, clinical tone is a mediocre product. The system prompt is where you fix that. Tell the model to use markdown: headers, bullet points, bold for important numbers. This makes responses scannable rather than walls of text. Tell it to be friendly and conversational rather than formal. If the product allows it, instruct the model to use relevant emojis to highlight key information a check for completed actions, a warning for important caveats . These instructions are cheap to add and meaningfully improve how the output feels. Ask the agent to suggest follow-up prompts at the end of responses. Something like: "After answering, offer 2-3 natural follow-up questions as a bulleted list, phrased as if the user is asking them." Users rarely know what to ask next, and this turns the agent from a lookup tool into something that feels like an actual conversation. And do not be afraid to give the model a personality. "Be warm, curious, and occasionally funny" in the system prompt will produce noticeably different and usually better interactions than nothing at all. The goal is an agent that feels like a helpful colleague, not a form you submit queries to. Let the SDK Run the Loop: the Tool Runner Once your tools are classes, the SDK can run the entire agent loop for you. The tool runner calls the model, executes any tools the model requests, feeds the results back, and continues until the model produces a final answer, all without you hand-writing the loop: runner = ANTHROPIC.beta.messages.tool runner model: "claude-sonnet-4-6", max tokens: 1024, messages: { role: "user", content: "What does customer 4471 still owe?" } , tools: LookupInvoices.new runner.each message do |message| Each turn of the conversation streams through here: assistant tool-use requests, your tool results, and the final answer. Rails.logger.info message.content end This is the right default for most agents, because the loop logic is identical across every agent and there is no value in reimplementing it. Write the loop by hand only when you need something the runner does not support, such as injecting a human approval step in the middle, enforcing a custom stopping condition, or persisting state between turns in a specific way. One production note: the tool runner lives under the beta.messages namespace. Anything under beta can move between releases, so pin your version and read the changelog before upgrading. Using MCP Servers as Tools You do not have to hand-write every tool as a Ruby class. If a capability already exists behind a Model Context Protocol MCP server, the Anthropic API can connect to it for you and expose its tools to the model directly. You declare the server in the request, and Anthropic makes the connection and runs the tool calls server-side. Your agent loop never sees them: the results come back as content blocks in the same response, the way a server-side tool does. This is the MCP connector, and it takes two pieces that must agree. List the server under mcp servers , then reference it by name with an mcp toolset entry in tools . Omit either and the request is rejected. response = ANTHROPIC.beta.messages.create model: "claude-sonnet-4-6", max tokens: 1024, betas: "mcp-client-2025-11-20" , mcp servers: { type: "url", name: "inventory", url: "https://mcp.internal.example.com/sse", Sent to the MCP server, not stored on any agent definition. authorization token: Rails.application.credentials.dig :mcp, :inventory token } , tools: Must reference a server by the exact name above. { type: "mcp toolset", mcp server name: "inventory" } , messages: { role: "user", content: "How many units of SKU-4471 are in the Dubai warehouse?" } The connector lives under the beta.messages namespace and needs the mcp-client-2025-11-20 beta flag, so pin your gem version. The same beta and parameter shape work with the tool runner: pass mcp servers and the mcp toolset entry to tool runner and the model can interleave MCP tool calls with your own Ruby tools in a single loop. By default the toolset exposes every tool the server advertises. To allowlist, flip the default off and opt in per tool, the same shape the agent toolset uses: tools: { type: "mcp toolset", mcp server name: "inventory", default config: { enabled: false }, configs: { name: "lookup stock", enabled: true } } Two cautions are worth stating plainly. First, the connection is made from Anthropic's infrastructure, so the MCP endpoint has to be reachable from outside your network and properly authenticated. If a server should never leave your VPC, do not expose it this way; run your own MCP client behind the firewall and surface its tools as ordinary Ruby tool classes instead, so the traffic stays inside your perimeter. Second, everything an MCP tool returns is untrusted external content the same as any other tool result, and the prompt-injection defenses later in this post apply to it without exception. A third-party MCP server is a trust boundary; treat its output as data, never as instructions. Cost Saving Strategies Tokens cost money and latency costs users. The two most effective levers are model routing and prompt caching. Route by Model Capability Not every step of an agent needs your most capable model. Using Sonnet everywhere is how costs balloon. Haiku is fast and inexpensive; Sonnet is the balanced workhorse; Opus handles hard reasoning. Route by difficulty. A ModelRouter uses Haiku to classify the incoming request, then dispatches it to the appropriate model or agent path. Classification is cheap, and it keeps the expensive model reserved for tasks that actually need it. class ModelRouter ROUTING PROMPT = <<~PROMPT Classify this user request into one of these categories: - simple: factual lookup, status check, or single-tool call - complex: multi-step reasoning, synthesis across multiple data sources - sensitive: involves money, account deletion, or escalation to a human Reply with only the category name. PROMPT def self.route user message response = ANTHROPIC.messages.create model: "claude-haiku-4-5-20251001", Use the cheapest model for classification max tokens: 10, messages: { role: "user", content: " {ROUTING PROMPT}\n\nRequest: {user message}" } case response.content.first.text.strip when "simple" then "claude-haiku-4-5-20251001" when "complex" then "claude-sonnet-4-6" when "sensitive" then "claude-opus-4-8" else "claude-sonnet-4-6" end end end Usage: pick the model before starting the agent loop model = ModelRouter.route user message runner = ANTHROPIC.beta.messages.tool runner model: model, messages: messages, tools: tools The cost of the classification call is tiny. If most of your requests are simple lookups, this can cut model spend significantly. Use Prompt Caching If your system prompt or tool definitions are long and stable and they usually are , prompt caching can cut costs by up to 90% and reduce latency by up to 85% on repeated calls. The API caches prompt prefixes marked with cache control , and you only pay full price for the cached portion on the first call. ANTHROPIC.messages.create model: "claude-sonnet-4-6", max tokens: 1024, system: { type: "text", text: LONG SYSTEM PROMPT, cache control: { type: "ephemeral" } Cache this prefix across requests } , messages: conversation.to messages The cache is keyed to the exact prefix content. As long as your system prompt does not change between requests, subsequent calls pay only 10% of the normal input price for the cached portion. This is especially valuable for agents with detailed tool descriptions or large context documents injected into the system prompt. Keep Context Lean Every token in the conversation history is a token you pay to process on every subsequent turn. Long-running agent sessions accumulate history fast. Periodically summarize old turns rather than feeding the full history into every call. The max tokens parameter on individual calls and an iteration cap on the agent loop are the two cheapest guardrails to add. Streaming for Responsive Interfaces If your agent talks to a user in real time, stream tokens as they are generated rather than making the user wait for the full response. The SDK supports server-sent events: stream = ANTHROPIC.messages.stream model: "claude-sonnet-4-6", max tokens: 1024, messages: { role: "user", content: "Draft a payment reminder email." } full text = +"" stream.text.each do |chunk| full text << chunk Append each token to the message bubble as it arrives. The container a div with dom id "message