35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Ruby MCP Server: Bouw een Model Context Protocol Server in Rails voor Claude en Cursor

Ruby MCP Server: Bouw een Model Context Protocol Server in Rails voor Claude en Cursor

Roger Heykoop
Ruby on Rails, AI
Ruby MCP Server in Rails: bouw een Model Context Protocol server, ontsluit tools en resources naar Claude en Cursor met auth en productie-patronen binnenin.

Een founder waar ik als fractional CTO mee werk heeft een Rails 8 SaaS die logistiek beheert voor middelgrote Europese retailers. Vorige maand begon zijn ops team te vragen of “Claude even in ons systeem kan kijken naar shipments.” Hij had nog een interne chatbot endpoint kunnen bouwen, function calling kunnen optuigen, en daar een sprint aan kunnen besteden. In plaats daarvan hebben we in drie middagen een Ruby MCP Server gelanceerd die nu rechtstreeks koppelt met Claude Desktop, Cursor en de Anthropic API aan de backend kant. Hetzelfde tool-oppervlak, drie verschillende clients, geen lijmcode per client.

Na negentien jaar Rails word ik niet vaak enthousiast van nieuwe protocollen. JSON-RPC over stdio is niet nieuw. REST met auth is niet nieuw. Maar het Model Context Protocol is de eerste keer dat ik een standaard zo snel zie aanslaan dat het echt de moeite waard is om er direct tegenaan te bouwen. Als je Rails app iets bevat dat een LLM zou willen lezen of doen, dan moet je het ontsluiten als een Ruby MCP Server. Dit is het productie-playbook.

Wat het Model Context Protocol Eigenlijk Is

Het Model Context Protocol, kortweg MCP, is een JSON-RPC 2.0 protocol waarmee een LLM client en een tool server in één gedeelde vorm met elkaar praten. De server adverteert capabilities — tools die het model kan aanroepen, resources die het model kan lezen, en prompts die het model kan gebruiken — en de client roept ze aan namens het model. Anthropic lanceerde de oorspronkelijke spec eind 2024, en halverwege 2025 ondersteunde elke serieuze LLM client het: Claude Desktop, Claude Code, Cursor, ChatGPT en de meeste agent frameworks.

Het punt van MCP is niet wat het doet, het is wat het vervangt. Zonder MCP is elke integratie tussen een model en een systeem maatwerk. Je schrijft een function-calling schema voor de ene client, een custom OpenAPI tussenlaag voor de andere, en een plugin manifest voor de derde. Met een Ruby MCP Server schrijf je de integratie één keer, en elke compliant client praat ermee. De Rails app houdt op “een integratie” te zijn en wordt een gelijkwaardige speler in het LLM ecosysteem.

Drie primitieven tellen in de praktijk. Tools zijn getypte function calls — geef het model een naam, een JSON schema voor argumenten, en een Ruby method die een resultaat teruggeeft. Resources zijn adresseerbare read-only documenten — bestanden, database records, of wat het model ook in zijn context wil laden. Prompts zijn herbruikbare templated instructies die de gebruiker vanuit de client UI kan triggeren. De meeste productie-servers die ik heb gebouwd leunen zwaar op tools, soms op resources, en zelden op prompts.

Waarom een Ruby MCP Server in Rails Logisch Is

De reflex wanneer een Rails team AI features toevoegt is om vanuit Rails uit te bellen naar het model. De Ruby MCP Server keert dat om. Het model belt naar Rails toe. Waarom is dat belangrijk? Omdat het meeste wat het model nodig heeft achter je authorization laag, je scopes, je audit log en je domeinlogica leeft. Dat oppervlak nabouwen in een Python sidecar om een LLM te voeden is hoe teams per ongeluk eindigen met twee source-of-truth systemen.

Een Ruby MCP Server binnen dezelfde Rails app hergebruikt alles. Pundit policies blijven gelden. Current account scoping blijft gelden. Background jobs die je al hebt blijven werken. De MCP laag is een dunne adapter — hij vertaalt een JSON-RPC tool call naar een service object call, en vertaalt het resultaat terug. Dat is het hele werk. Het zware werk — domeinlogica, validaties, autorisatie — zit al in je app.

De andere reden dat dit logisch is in 2026 is dat de Ruby MCP SDK eindelijk goed is. De officiële mcp gem van de Model Context Protocol working group landde vorig jaar, en hij regelt transports (stdio, SSE, streamable HTTP), capability negotiation en JSON-RPC framing voor je. Je schrijft Ruby classes die tools en resources beschrijven, en de SDK doet het wire-werk.

Een Ruby MCP Server Opzetten met de SDK

De minimale Ruby MCP Server is ongeveer dertig regels. Voeg de gem toe, definieer een tool class, registreer hem, draai de server. Ik laat eerst de stdio transport zien omdat die het makkelijkst is om tegen Claude Desktop te testen, en daarna de streamable HTTP transport voor productie.

# Gemfile
gem "mcp", "~> 0.5"
# lib/mcp_server/tools/lookup_shipment.rb
module McpServer
  module Tools
    class LookupShipment < MCP::Tool
      description "Look up a shipment by tracking number. Returns status, carrier, and last scan."

      input_schema do
        required(:tracking_number).filled(:string)
      end

      def call(tracking_number:, server_context:)
        account = server_context.fetch(:account)
        shipment = account.shipments.find_by(tracking_number: tracking_number)
        return MCP::Tool::Response.new([{ type: "text", text: "Not found" }]) unless shipment

        MCP::Tool::Response.new([{
          type: "text",
          text: ShipmentSerializer.new(shipment).to_json
        }])
      end
    end
  end
end

De server_context is de brug tussen het protocol en je Rails app. Wat je daar bij server-boot in stopt, is binnen elke tool call beschikbaar. Daar zet je het authenticated account in, de current user, het request id — alles wat je normaal vanuit een controller uit current_user zou lezen.

De server zelf opstarten is één bestand:

# bin/mcp_server
#!/usr/bin/env ruby
require_relative "../config/environment"

server = MCP::Server.new(
  name: "ttb-logistics",
  version: "1.0.0",
  tools: [
    McpServer::Tools::LookupShipment,
    McpServer::Tools::CreatePickup,
    McpServer::Tools::UpdateAddress
  ],
  server_context: { account: Account.find_by!(api_key: ENV.fetch("MCP_ACCOUNT_KEY")) }
)

MCP::Transports::Stdio.new(server).open

Maak hem executable, wijs Claude Desktop ernaar via een config entry, en het model kan nu lookup_shipment aanroepen vanuit een chat. Restart de desktop client, vraag “wat is de status van TR-12345”, en de tool gaat af. De totale Ruby footprint is het schema, de call method, en een eenregelige registratie.

Rails Resources Ontsluiten via MCP

Tools zijn de werkpaarden, maar resources zijn waar een Ruby MCP Server begint te voelen als iets anders dan een function-calling endpoint. Een resource is alles wat het model als read in zijn context kan laden. Het MCP protocol geeft elk een URI, een MIME type en een optionele list. De client beslist wanneer ze worden opgehaald — soms kiest de gebruiker er expliciet één uit de UI, soms vraagt het model erom als onderdeel van een flow.

In een Rails app zijn resources meestal views op je records. Een customer record. Een order. Een document. Een markdown export van een wiki pagina. Alles wat read-only is en profiteert van in de context van het model worden geladen.

# lib/mcp_server/resources/customer_resource.rb
module McpServer
  module Resources
    class CustomerResource < MCP::Resource
      uri_template "ttb://customers/{id}"
      name "Customer record"
      mime_type "application/json"

      def list(server_context:)
        account = server_context.fetch(:account)
        account.customers.order(updated_at: :desc).limit(50).map do |c|
          {
            uri: "ttb://customers/#{c.id}",
            name: c.display_name,
            description: "Customer #{c.id} (#{c.tier})"
          }
        end
      end

      def read(uri:, server_context:)
        account = server_context.fetch(:account)
        id = uri.match(%r{ttb://customers/(\d+)})[1]
        customer = account.customers.find(id)

        [{
          uri: uri,
          mime_type: "application/json",
          text: CustomerSerializer.new(customer).to_json
        }]
      end
    end
  end
end

De list method is wat de resource picker in de client UI vult. De read method is wat er één ophaalt. Beide draaien binnen de Rails request lifecycle, beide gaan door Pundit als je dat wil, en beide kunnen ActiveRecord, Solid Cache of wat dan ook in je app aanroepen.

De truc voor goede resources is om na te denken over wat het model wil laden, niet wat een API zou teruggeven. Een model dat een customer laadt wil de lifetime value, de laatste drie orders, de open tickets en de account notes inline hebben. Niet alleen de rij uit de customers tabel. Behandel de serializer als een context-pack, niet als een JSON dump.

Tools Bouwen die op Je Domein Opereren

Een read-only Ruby MCP Server is nuttig. Een read-write versie is waar het gevaarlijk wordt. Het model gaat cancel_subscription aanroepen zodra de gebruiker “cancel mijn abonnement” intypt in chat, en je wilt niet dat het uitvoert tegen het verkeerde account, met stale argumenten, of zonder audit trail.

Drie patronen maken read-write tools veilig. Ten eerste, scope elke tool aan het authenticated account. Ten tweede, route de tool call door je bestaande service objects, niet door een vers code-pad. Ten derde, log elke tool aanroep naar een audit tabel met de input, output en een correlation id.

module McpServer
  module Tools
    class CancelSubscription < MCP::Tool
      description "Cancel a subscription at the end of the current period."

      input_schema do
        required(:subscription_id).filled(:string)
        optional(:reason).maybe(:string)
      end

      def call(subscription_id:, reason: nil, server_context:)
        account = server_context.fetch(:account)
        subscription = account.subscriptions.find(subscription_id)

        result = Subscriptions::CancelService.call(
          subscription: subscription,
          reason: reason,
          actor: server_context.fetch(:actor),
          source: :mcp
        )

        AuditLog.create!(
          account: account,
          action: "mcp.cancel_subscription",
          subject: subscription,
          payload: { subscription_id: subscription_id, reason: reason },
          result: result.success? ? "ok" : "error"
        )

        if result.success?
          MCP::Tool::Response.new([{ type: "text", text: "Cancelled at #{result.cancels_at}" }])
        else
          MCP::Tool::Response.new([{ type: "text", text: "Error: #{result.error}" }], is_error: true)
        end
      end
    end
  end
end

Let op het source: :mcp argument dat de service ingaat. Dat is het audit-broodkruimel dat je wilt wanneer iemand zes maanden verder vraagt waarom een abonnement geannuleerd is en het enige spoor is “de assistent deed het.” Een correlation id uit server_context werkt ook. Het punt is dat het model nooit onzichtbaar moet zijn in je data. Elke wijziging die hij maakt moet getagd zijn.

Dit patroon is hetzelfde dat ik beschreef in Rails webhook processing met idempotency — de Ruby MCP Server is gewoon nog een inkomend kanaal, en verdient dezelfde idempotency, audit en autorisatie discipline als een Stripe webhook.

Authenticatie en Beveiliging voor MCP Servers

Stdio is prima voor desktop clients die op dezelfde machine als de gebruiker draaien. Voor al het andere — een hosted MCP server waar Cursor, ChatGPT of je eigen backend over het netwerk mee praat — wil je streamable HTTP met fatsoenlijke auth. De MCP spec ondersteunt OAuth 2.1 met dynamic client registration, maar in de praktijk gebruiken de meeste productie-servers die ik nu zie bearer tokens die gescoped zijn aan een account.

Hier is de vorm van een Rack-mounted Ruby MCP Server binnen Rails:

# config/routes.rb
mount McpServer::RackApp.new => "/mcp"
# lib/mcp_server/rack_app.rb
module McpServer
  class RackApp
    def call(env)
      request = Rack::Request.new(env)
      token = request.get_header("HTTP_AUTHORIZATION")&.sub(/\ABearer /, "")
      account = Account.find_by(mcp_token: token)
      return [401, { "content-type" => "text/plain" }, ["unauthorized"]] unless account

      Rails.application.executor.wrap do
        ActsAsTenant.with_tenant(account) do
          server = MCP::Server.new(
            name: "ttb-logistics",
            version: "1.0.0",
            tools: McpServer.tools,
            resources: McpServer.resources,
            server_context: { account: account, actor: account.mcp_actor }
          )
          MCP::Transports::StreamableHttp.new(server).call(env)
        end
      end
    end
  end
end

Een paar dingen tellen hier. De Rails.application.executor.wrap zorgt dat connection pooling en reloading hetzelfde werken als een normaal Rails request. De ActsAsTenant.with_tenant geeft je database-niveau scoping als je multi-tenant draait — dezelfde defence-in-depth die ik behandelde in Pundit autorisatie voor multi-tenant SaaS. En de bearer token is per account roteerbaar, wat betekent dat toegang intrekken tot je Ruby MCP Server een enkele SQL update is.

Zet het hele ding achter Rack::Attack om misbruikende clients te throttlen, en achter een feature flag zodat je het account voor account kan uitrollen. Ik behandelde dat patroon in Rails feature flags met Flipper — hetzelfde playbook geldt.

De Ruby MCP Server in Productie Deployen

Er zijn twee deployment vormen. Als je Ruby MCP Server HTTP-based is, deployt hij precies zoals de rest van je Rails app. Dezelfde Puma config, dezelfde Kamal setup, dezelfde observability. Het enige om toe te voegen is een aparte route prefix in je access logs zodat je MCP traffic apart van menselijke traffic kunt graphen — die heeft hele andere latency en concurrency patronen.

Als je Ruby MCP Server stdio-based is, heb je twee opties. Je kunt een klein binary uitleveren dat klanten lokaal draaien (goed voor desktop integraties), of je kunt stdio binnen een hosted process wrappen en het aan de edge over SSE of HTTP exposen. Voor de meeste B2B SaaS is hosted HTTP het antwoord. Klanten willen geen binary installeren, en jij wilt dat elke call in je observability stack landt.

Een paar productie-lessen. Ten eerste, zet per-tool timeouts binnen de tool — vertrouw niet op de client om te time-outen, want sommige clients wachten eeuwig. Ten tweede, return gestructureerde errors met is_error: true in plaats van te raisen — het model gaat veel beter om met gestructureerde errors dan met een stack trace in de response. Ten derde, houd tool descriptions kort en gedragsmatig. “Cancel a subscription at the end of the current period” verslaat “Cancels the given subscription using the standard cancellation flow as defined by the billing service.”

Die laatste telt zwaarder dan je denkt. Tool descriptions zijn waar het model op af gaat om te beslissen of het je tool aanroept. Het is prompt engineering, geen API documentatie. Itereer erop zoals je op een system prompt itereert. Ik houd meestal een fixture file bij met realistische user messages en draai die door de client om te zien welke tool het model kiest, op dezelfde manier als ik routing in een controller zou testen.

FAQ

Wat is het verschil tussen een Ruby MCP Server en Claude function calling?

Function calling is een per-request schema dat je naar de Anthropic API stuurt. Een Ruby MCP Server is een long-lived service waar elke compliant client mee kan verbinden. De server-side code is vergelijkbaar — beide eindigen met het aanroepen van een Ruby method met getypte argumenten — maar MCP geeft je discovery, lifecycle en multi-client support out of the box, waar function calling vereist dat je elke client handmatig aansluit. Ik behandelde de function-calling route in LLM function calling in Rails als je de vergelijking wilt.

Kan ik een Ruby MCP Server gebruiken met andere clients dan Claude?

Ja. MCP is een client-neutrale standaard. Cursor, ChatGPT, Continue en de meeste open-source agent frameworks ondersteunen het. Dezelfde Ruby MCP Server die je aan Claude Desktop ontsluit, kan ingeplugd worden in Cursor voor code-aware tooling, of in een custom Rails-side agent die de Anthropic SDK als model gebruikt. Dat is het hele punt — schrijf de integratie één keer, krijg elke client.

Hoe test ik een Ruby MCP Server?

De MCP SDK levert een in-process test harness mee waarmee je tools en resources vanuit RSpec kunt aanroepen zonder via stdio of HTTP te gaan. Gebruik die voor unit tests van je tool-logica. Voor integratie tests, draai de server met de stdio transport en gebruik de officiële MCP Inspector CLI om hem aan te sturen. Ik houd een feature spec bij die de server start, tools opsomt, elke tool met een bekende input aanroept, en de response shape assert. Hij vangt schema drift snel.

Is een Ruby MCP Server veilig om aan het publieke internet te ontsluiten?

Dat kan, met dezelfde controles die je op elke API zou zetten. Bearer auth, per-account rate limiting, audit logging op elke tool call, idempotency keys op writes, en feature-flag gating per account. Het risicomodel is hetzelfde als een gewone API — het verschil is dat de consumer een LLM is, wat betekent dat je moet aannemen dat hij je tools in onverwachte volgorde met onverwachte argumenten aanroept. Behandel tool inputs zoals je user input behandelt. Valideer, scope en log.

Hulp nodig met het bouwen van een Ruby MCP Server, het integreren van Claude in je Rails app, of een AI roadmap die geen plankware wordt? TTB Software is gespecialiseerd in pragmatische AI integratie voor Rails teams. We doen Rails al negentien jaar, en AI in Rails sinds de dag dat Anthropic de SDK lanceerde.

#ruby-mcp-server #rails-mcp-server #model-context-protocol-ruby #claude-mcp-integration #mcp-server-tutorial #rails-ai-integration #ruby-on-rails
R

About the Author

Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation