Building Uscreen’s remote, multi-tenant MCP server inside a Rails app Evil Martians moved Uscreen's Model Context Protocol server from a locally installed NPM package into Uscreen's existing Rails application, letting creators connect an AI client by pasting a URL, signing in, and approving access instead of installing software and managing API keys. The remote, multi-tenant server exposes 57 tools that call the same Rails models and business logic as the product, keeps every request scoped to the creator's own Account and approved permissions, and previews the effect of destructive tools by default, such as how many subscribers a deleted tag would affect. The implementation pairs an OAuth server for sign-in with a library for account scoping, and Evil Martians says the same approach works for any Rails product that wants to give users AI access to their accounts. Building Uscreen’s remote, multi-tenant MCP server inside a Rails app Uscreen https://www.uscreen.tv , the leading all-in-one membership platform for video creators and entrepreneurs, came to Evil Martians with a working Model Context Protocol MCP server already tested by early adopters. To use it, a creator installed an NPM package that ran the server on their computer, entered an API key, and plugged it into the AI client of their choice. It worked, but it took a lot of setup. So, to make creators’ lives easier, Evil Martians moved the server into Uscreen’s Rails application. Now, all a creator needs to do is add the Uscreen MCP URL to their AI client, sign in, and approve access. This post covers how that implementation works: authenticating a creator, keeping requests inside the right account, defining tools around existing Rails models, and previewing destructive changes. Let’s take Fit with Coco https://fitwithcoco.com/ as an example; it’s a fitness membership with hundreds of Pilates, strength, and cardio workouts, multi-week challenges, and a seven-day free trial. Now, imagine running all of that through an admin panel. The users with programs like this can be expected to know the perfect plank, but not necessarily their way around Vim. The panel handles individual edits and some bulk actions, but plenty of tasks span many records and can turn into an evening of scrolling, filtering, and ticking checkboxes. For example: - Updating descriptions and tags for every workout in a six-week challenge. - Finding workouts with the most watch time in a week and comparing monthly recurring revenue with last month. - Listing subscriptions scheduled to cancel at the end of the billing period and free trials ending this week, grouped by plan. These were the kinds of tasks we wanted creators to describe once rather than complete item by item . And thankfully, MCP gives an AI client a way to request the necessary application operations. - Uscreen creators can connect an AI client to their account by pasting a URL, signing in, and approving access. They don’t install anything or manage API keys. - We built the MCP server into Uscreen’s existing Rails application. Its 57 tools use the same models and business logic as the product, and new tools ship with regular deploys. - Every request stays inside the creator’s own account and the permissions they approved. - Destructive tools preview their effect by default, such as how many subscribers a deleted tag would affect. - This same approach works for any Rails product that wants to give its users AI access to their accounts. Table of contents : The original server ran locally on each creator’s computer. Creators had to install and configure it, and every new tool came with a package update. Hosting the server removed that work on its own. Where to host was a separate decision. The local server talked to Uscreen through its public API, so it could only do what the API already offered. A new tool often meant adding a public endpoint first, and every public endpoint is a contract Uscreen has to keep stable for all the integrations built on it. Putting the server inside the Rails application removed that step: Inside Rails, a tool is one more entry point to the application, like a controller. It calls the same models and services as the API and the admin panel, and it ships with the application instead of waiting for a new endpoint and a package release. A remote server also takes on new responsibilities. It has to know who is behind each request the creator or a team member with admin access , which Account they’re acting for, and what they’re allowed to do. Uscreen is multi-tenant: each creator works in a space of their own, an Account , that holds their videos, subscribers, and everything else they’ve built. Every request had to stay inside the right one. That meant two pieces of infrastructure: an OAuth server to sign people in and a library to handle MCP requests. For the MCP library, we had two options at the time: the official mcp gem https://github.com/modelcontextprotocol/ruby-sdk and fast-mcp https://github.com/yjacquin/fast-mcp . The official gem won on three counts: - Transport. It supported Streamable HTTP, the standard transport for remote MCP servers, with a stateless mode, so any application instance can handle any request. fast-mcp only offered the older HTTP+SSE transport, which keeps each client’s stream in one process’s memory. - Per-request context. The official gem passes a server context with the Account, user, and token into every tool call. fast-mcp tools only receive the request headers, so each tool would have to look up the token again. - Maintenance. The official gem is maintained by the MCP project alongside the specification and was updated often. fast-mcp hadn’t had a release since September 2025. For authentication, the MCP specification builds on OAuth 2.1, and the Rails ecosystem has a battle-tested component for that: Doorkeeper https://github.com/doorkeeper-gem/doorkeeper . Uscreen had even used it before, for an older version of its API, so bringing it back meant reusing the OAuth tables already in the schema rather than starting from scratch: ~/Gemfile gem "doorkeeper" gem "mcp", "~ 1.1.0" The official gem gives each tool a MCP::Tool class with a name, a description, an input schema, and a call method. Every Uscreen tool inherits from one more layer, Mcp::ApplicationTool , the same way Rails models inherit from ApplicationRecord : ~/app/lib/mcp/application tool.rb module Mcp class ApplicationTool < MCP::Tool include ActiveSupport::Rescuable rescue from ActiveRecord::RecordNotFound do |error| failure response " {error.model.underscore} not found" end class << self def description from template locals namespace = module parent.name.delete prefix "Mcp::Tools" .delete prefix "::" .underscore template = Rails.root.join "app/lib/mcp/descriptions", namespace, " {tool name}.txt.erb" .read description ERB.new template, trim mode: "-" .result with hash locals end def call server context:, args verify scope server context perform server context: server context, args rescue = error rescue with handler error || raise end def respond with resource, options body = ActiveModelSerializers::SerializableResource.new resource, options .as json MCP::Tool::Response.new { type: "text", text: body.to json } end def failure response error code body = { error: error code, message: I18n.t "mcp.errors. {error code}" } MCP::Tool::Response.new { type: "text", text: body.to json } , error: true end Scope checks: requires scope, verify scope , ... end end end With that in place, a tool’s perform only contains its own logic: python ~/app/lib/mcp/tools/cms/videos/get tool.rb def perform server context:, video id: video = server context :account .videos.not deleted.find video id respond with video end respond with finds the model’s serializer and wraps the result in an MCP response. A missing ID raises ActiveRecord::RecordNotFound , and the base class turns it into a video not found error. The same happens when the ID belongs to another Account, because the lookup starts from the Account in server context . server context carries the Account, user, and scopes from the access token; we’ll show how it gets there in Authorization in MCP 101 authorization-in-mcp-101 . Each tool tells the model what arguments it takes, when to use it, and how it behaves. Here’s the rest of videos get , whose perform we saw above: ~/app/lib/mcp/tools/cms/videos/get tool.rb class GetTool < Mcp::ApplicationTool tool name "videos get" description from template annotations title: "Get video", read only hint: true, destructive hint: false, idempotent hint: false, open world hint: false input schema properties: { video id: { type: "integer", minimum: 1, description: "Internal video id. Get it from videos list response data .id ." } }, required: "video id" requires scope Mcp::Scopes::CMS READ ... end The input schema declares the arguments a tool accepts. Its descriptions can also tell the model where those arguments should come from: here, video id points the caller to videos list rather than asking it to infer an ID from a title. The tool description answers a different question: when should this tool be used? We keep those descriptions in ERB templates, separate from the Ruby implementation, so they can distinguish related operations such as fetching one video, listing videos, or fetching a collection. Here’s the full videos get description: <% ~/app/lib/mcp/descriptions/cms/videos/videos get.txt.erb %