Architecture And Connection Flow

Core Idea

This implementation keeps MCP inside the Rails app rather than running a separate MCP service. The app mounts Rack endpoints that authenticate a token, build an MCP::Server, and delegate JSON-RPC requests to the Ruby mcp gem.

There are two servers:

  1. Organization MCP at /mcp/:token
  2. Admin MCP at /admin/mcp/:token

Both share the same token table and activity log table.

Runtime Layers

AI client
  -> POST /mcp/:token or /admin/mcp/:token
  -> Rack app authenticates token
  -> builder selects permitted tool classes
  -> MCP::Server receives tools/list or tools/call
  -> tool class runs against scoped app models
  -> response returns as JSON-RPC payload
  -> activity log stores the call summary

Shared Runtime Components

Rack base

Mcp::BaseRackApp owns the protocol-agnostic transport concerns:

  • extract token from the URL path
  • look up the raw token through a SHA256 digest
  • enforce a per-token rate limit
  • reuse a StreamableHTTPTransport instance per token
  • build a fresh server on each request
  • normalize 401, 429, and 500 JSON-RPC error responses

Token model

Mcp::Token is the central access primitive for both MCPs.

It stores:

  • token_digest
  • token_prefix
  • scope
  • permissions
  • revoked_at
  • last_used_at

The raw token is never stored. It is shown once when created or regenerated.

Activity log

Mcp::ActivityLog records:

  • tool name
  • domain
  • action type
  • sanitized arguments
  • preview of the response body
  • owning token
  • organization when applicable

Organization MCP Flow

Request lifecycle

  1. Client sends POST /mcp/:token
  2. Mcp::RackApp resolves Mcp::Token.find_by_raw_token(raw_token)
  3. Authorization checks that the token belongs to an organization with a marketplace
  4. Mcp::OrganizationContext is created with the organization and token
  5. Mcp::ServerBuilder.build(context) filters tool classes using token permissions
  6. The transport dispatches tools/list or tools/call
  7. The tool reads organization and marketplace through Mcp::BaseTool
  8. The response is logged and returned to the client

Why the org MCP is safe by structure

The tools do not receive an organization id from the caller. The organization is resolved before tool execution and injected into thread-local context. A tool can only access the current tenant unless the tool author bypasses the scoped associations.

Admin MCP Flow

The admin server uses the same Rack and token pattern with a thinner context model:

  1. Client sends POST /admin/mcp/:token
  2. Mcp::AdminRackApp accepts only scope: "admin" tokens
  3. Mcp::Admin::ServerBuilder filters admin tools using the same permission model
  4. Mcp::Admin::BaseTool stores the token in a thread-local slot
  5. Tools run against platform-wide models

The key design difference is scope, not transport.

How It Connects To AI Clients

This implementation relies on Streamable HTTP transport, so MCP hosts only need a URL.

Claude Desktop or Claude Code

{
  "mcpServers": {
    "marketplace": {
      "url": "https://your-app.com/mcp/YOUR_TOKEN"
    }
  }
}

Protocol exchange

  • tools/list lets the client discover the tool catalog
  • tools/call invokes a tool by name with a JSON argument object
  • tool descriptions and input schemas are what guide the model during tool selection

That means the real AI integration is not a separate adapter layer. It is the combination of:

  • clear tool names
  • precise descriptions
  • small, explicit input schemas
  • consistent response formats

Transport And Server Construction Details

Per-request server construction

The builder creates a new MCP::Server for each request so permission filtering always reflects the current token state.

Per-token transport reuse

The transport object is cached by token id inside the Rack app. The server instance inside that transport is swapped on each request. This reduces transport setup churn without sharing stale tool state.

Thread-local context

Both base tool classes use Thread.current rather than ordinary class variables. That prevents request leakage when the app runs under Puma or any threaded server.

Decision Summary

Use this structure again when the target project needs:

  • first-class MCP endpoints inside the main app
  • tenant isolation enforced before tool execution
  • multiple tokens with different permission sets
  • AI clients that can connect by URL only

Do not use this exact structure unchanged if the target project needs:

  • websocket-first transport
  • per-user tool context rather than per-tenant context
  • externalized token auth at an API gateway
  • a non-Rails stack