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:
- Organization MCP at
/mcp/:token - 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
StreamableHTTPTransportinstance 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_digesttoken_prefixscopepermissionsrevoked_atlast_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
- Client sends
POST /mcp/:token Mcp::RackAppresolvesMcp::Token.find_by_raw_token(raw_token)- Authorization checks that the token belongs to an organization with a marketplace
Mcp::OrganizationContextis created with the organization and tokenMcp::ServerBuilder.build(context)filters tool classes using token permissions- The transport dispatches
tools/listortools/call - The tool reads
organizationandmarketplacethroughMcp::BaseTool - 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:
- Client sends
POST /admin/mcp/:token Mcp::AdminRackAppaccepts onlyscope: "admin"tokensMcp::Admin::ServerBuilderfilters admin tools using the same permission modelMcp::Admin::BaseToolstores the token in a thread-local slot- 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/listlets the client discover the tool catalogtools/callinvokes 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