Project · Tool calling and MCP client in Ruby on Rails

ai_shop_assistant

A shopping chat built with Rails 8 where the AI decides when to search Shopify's live Global Catalog over MCP and answers with real products, prices and links.

Ruby 4.0 · Rails 8.1 · RubyLLM · Gemini · Hotwire · Solid Queue · Solid Cable · MongoDB · RSpec

The problem

An AI model with no way to look things up will happily invent a product, a price or a link. For shopping that is a real problem: a made-up price is worse than no answer.

What I built

A Rails chat app that gives the model one tool, SearchProducts, and lets it decide when to use it. When a question needs real product data, the model asks for the tool, the app searches Shopify’s live Global Catalog, and the model answers from what came back: up to three products, each reduced to a title, a price and a URL.

It is the same tool calling pattern behind the shopping features in ChatGPT, Perplexity and Copilot, built from scratch in Ruby.

How it works

Flow diagram of ai_shop_assistant in six steps: 1. the shopper sends a message in the browser, 2. the Rails controller saves it and enqueues a job, 3. a Solid Queue background job runs the model, 4. Gemini, through RubyLLM, decides whether to search, 5. the SearchProducts tool queries Shopify's live Catalog over MCP, 6. the reply streams back to the browser as a Turbo Stream through Solid Cable.
  1. Save and enqueue. The controller saves the message and hands the slow model call to a background job, so the web request returns at once.
  2. Run the model in a job. ChatResponseJob on Solid Queue builds a RubyLLM chat with the SearchProducts tool attached.
  3. Let the model decide. If it asks for a search, the browser shows a “searching” status line.
  4. Search the live catalog. The tool gets an OAuth 2.0 client-credentials token and sends a JSON-RPC tools/call to Shopify’s catalog MCP endpoint.
  5. Stream the reply. The saved message broadcasts itself to the browser with a Turbo Stream.

What I verified

What broke, and what I learned

Replies never reached the browser. The job and the web server are separate processes, and Action Cable’s default async adapter only broadcasts inside one process. I switched to Solid Cable, which passes broadcasts through a shared database, with no Redis needed.

The assistant forgot the conversation. Each job builds a fresh chat, so on the second message the model had no memory of the first. The fix replays saved messages into the new chat, and a spec covers it (“My name is Alex”, then “What is my name?”).

An MCP gem crashed on the handshake. ruby_llm-mcp failed on Shopify’s response, so I filed ruby_llm-mcp#155 and called the endpoint directly with a small hand-written JSON-RPC client over Net::HTTP.

Approach: key decisions

Status

Works end to end: chat, live catalog search and streamed replies. It is a learning and portfolio project. It only searches (no cart or checkout) and returns at most three products per search.

Related services

Need something like this in your own product? See AI engineering and integration and Shopify, BigCommerce and e-commerce.

Other projects