45 MCP tools that give AI agents live access to your Rails schema, models, routes & conventions.
How rails-ai-context is built, and why.
graph TD
subgraph app["Your Rails App"]
A["models + schema + routes + controllers + views + jobs + config"]
end
A -->|"40 introspectors"| gem
subgraph gem["rails-ai-context"]
direction TB
subgraph engine["Introspection Engine"]
direction LR
I["Introspectors\n40 modules\nPresets\nCached"]
AST["AST Engine\nPrism\n44 listeners\nConfidence tags"]
H["Hydration Layer\nSchema hints\ninjected into\ntool responses"]
end
engine --> R["Tool Registry\n45 tools auto-discovered via inherited\n+ custom_tools - skip_tools = active tools"]
end
R --> MCP
R --> CLI
R --> S
subgraph outputs["Output"]
direction LR
MCP["MCP Server\nstdio / HTTP\nResources\nVFS URIs"]
CLI["CLI Runner\nRake / Thor\nSame 45 tools\nNo server needed"]
S["Serializers\n18 modules\nStatic files\nPer-AI-tool"]
end
style app fill:#4a9eff,stroke:#2d7ad4,color:#fff
style gem fill:#2d2d2d,stroke:#555,color:#fff
style engine fill:#3a3a3a,stroke:#666,color:#fff
style outputs fill:#1a1a1a,stroke:#444,color:#fff
style I fill:#6c5ce7,stroke:#5a4bd1,color:#fff
style AST fill:#e17055,stroke:#c0392b,color:#fff
style H fill:#00b894,stroke:#00a381,color:#fff
style R fill:#fdcb6e,stroke:#f0b429,color:#333
style MCP fill:#0984e3,stroke:#0770c2,color:#fff
style CLI fill:#00cec9,stroke:#00b5b0,color:#fff
style S fill:#a29bfe,stroke:#8c83f0,color:#fff
sequenceDiagram
participant AI as AI Assistant
participant MCP as MCP Server
participant TR as Tool Registry
participant Cache as Cache Layer
participant App as Rails App
AI->>MCP: rails_get_schema(table: "users")
MCP->>TR: resolve tool + execute
TR->>Cache: check introspection cache
alt cache hit (TTL + fingerprint valid)
Cache-->>TR: cached context
else cache miss
Cache->>App: introspect (40 modules)
App-->>Cache: structured data
Cache-->>TR: fresh context
end
TR-->>MCP: MCP::Tool::Response
MCP-->>AI: schema with columns, indexes, hints
flowchart LR
A[Tool Call] --> B{Registered?}
B -->|Yes| C{Cache Valid?}
B -->|No| E[ToolNotFoundError]
C -->|Hit| D[Return Cached]
C -->|Miss| F[Introspect]
F --> G[Cache Result]
G --> D
D --> H[Hydrate\nSchema Hints]
H --> I[Truncate\nmax_tool_response_chars]
I --> J[MCP::Tool::Response]
lib/rails_ai_context/introspectors/)40 modules that extract structured data from your Rails app. Each introspector:
Introspectors::Base, which holds the app handle and its rootIntrospector#call turns a raised section into { error: msg } and logs a warning, so one broken section costs only itselfINTROSPECTOR_MAP with a symbol key:standard, :full)The Introspector orchestrator runs configured introspectors and merges results.
Five modules answer questions every introspector used to answer for itself:
app/<kind>, packs, and the in-repo code roots read from the tree (a directory holding its own app/ plus a gemspec, plugin.rb or lib/**/engine.rb), memoized per rootPathResolver resolves: paths stats, each reads, classes names by the declared constantpackage.json: present means named in dependencies or devDependencies, so an overrides pin is not a dependency, and @tailwindcss/vite counts as tailwindcssOne more reads PostgreSQL’s catalogs for the schema, the database stats and rails_runtime_info:
pg_stat_user_tables and pg_stat_user_indexes, counting a partition toward its partitioned parent, so every surface names a partitioned table onceTwo more answer a question a tool asks:
skipped holds unconditional skips only; a skip carrying if:/unless: leaves the filter in own or inherited with the condition on the record. Every controller surface reads its filter line from here, so no two answers can disagreePrism AST parsing replaced all regex-based Ruby source parsing in v5.2.0.
Concurrent::Map), keyed by the SHA256 of the file’s content; a stat match answers without a read for a file already two seconds older than the read that recorded itMethodCallListener reports a named call with its arguments and options wherever one is asked for[VERIFIED] (static literals) or [INFERRED] (dynamic expressions), and a record in a static-tier entry is capped at [STATIC], since no record can claim more than the tier that carries itlib/rails_ai_context/tools/base_tool.rb)Auto-registration via Ruby’s inherited hook:
BaseTool.inherited(subclass) fires when any file in tools/ defines a subclass@descendants (protected by @registry_mutex)BaseTool.registered_tools eager-loads all tool files and returns non-abstract classesBaseTool itself is marked abstract! - excluded from the registryDeadlock-free design: eager_load! walks Tools.constants and const_gets each one without holding the mutex, because const_get triggers Zeitwerk autoloading which calls inherited, which takes the mutex itself.
lib/rails_ai_context/server.rb)Built on the official mcp Ruby SDK:
MCP::Server::Transports::StreamableHTTPTransport)ActiveSupport::Notificationslib/rails_ai_context/vfs.rb)Virtual File System for rails-ai-context:// URIs:
rails-ai-context://models/Post → model details + schema
rails-ai-context://controllers/Posts → actions, filters, params
rails-ai-context://controllers/Posts/index → action source
rails-ai-context://views/posts/show.html.erb → template content
rails-ai-context://routes/posts → filtered routes
Every resolve call introspects fresh - zero stale data.
lib/rails_ai_context/hydrators/)Cross-tool semantic hydration (v5.3.0):
@post → Post by convention, injects schema hintsSchemaHint value objects from cached contextResult: controller and view tools automatically include relevant schema information without extra tool calls.
lib/rails_ai_context/serializers/)18 modules that format introspection output for different AI tools:
| Serializer | Output |
|---|---|
ClaudeSerializer |
CLAUDE.md |
ClaudeRulesSerializer |
.claude/rules/*.md |
CursorRulesSerializer |
.cursor/rules/*.mdc AND .cursorrules (legacy chat-agent fallback) |
CopilotSerializer |
.github/copilot-instructions.md |
CopilotInstructionsSerializer |
.github/instructions/*.instructions.md |
OpencodeSerializer |
AGENTS.md (root) |
OpencodeRulesSerializer |
app/models/AGENTS.md, app/controllers/AGENTS.md |
JsonSerializer |
.ai-context.json |
MarkdownSerializer |
Base formatting |
ContextFileSerializer |
Atomic file writes with section markers |
CompactSerializerHelper |
Compact mode (≤150 lines) |
StackOverviewHelper |
Stack overview sections, and the rule-file write path four serializers share |
ToolGuideHelper |
MCP/CLI tool reference sections |
TestCommandDetection |
Test framework detection |
SectionFacts |
The facts every surface states about an app - auth, assets, associations, the filter chain, an unread entry’s row, the static-tier notice - each rendered in one place |
SectionMarkerWriter |
Writes a managed section into a file the user also owns |
ContextModeDispatch |
Picks full or compact rendering for a run |
Base |
Holds the context hash a serializer renders |
exe/rails-ai-context, lib/rails_ai_context/cli/)Thor-based CLI that works standalone (no Gemfile entry):
EntryBoot - Finds and boots the host Rails app for the standalone binary, or says which tier it fell back toToolRunner - Parses CLI args, resolves tool names, executes tools, formats output--json mode for machine-readable outputlib/rails_ai_context/safe_path.rb, lib/rails_ai_context/view_file.rb)BaseTool.safe_glob is the globbed-path form, checking realpath, containment and the sensitive realpath on each path a pattern yieldsFour cache layers:
BaseTool.SHARED_CACHE) - Mutex-protected, TTL + fingerprint invalidationAstCache) - Concurrent::Map, SHA256 fingerprint per file, a stat shortcut for a settled file, bounded at 500RunCache) - thread-local, lives for one Introspector#call or generate_context: file lists, stats and directory answers every section would otherwise ask againBaseTool.SESSION_CONTEXT) - Mutex-protected call history, resets on server restartLiveReload watches files and calls reset_all_caches! when changes are detected.
lib/rails_ai_context/fingerprinter.rb)SHA256-based change detection:
app/, config/, db/, lib/, rakelib/, test/, spec/, the Gemfile and Gemfile.lock (or gems.rb and gems.locked), package.json, tsconfig.json, config.ru, the Rakefilemcp gem’s MCP::Tool, MCP::Server, transports.schema.rb or structure.sql, or replays the migrations), without Brakeman, without ripgrep, without listen gem.require_relative is kept for the few files that load before or outside the loader.<!-- BEGIN/END rails-ai-context --> to preserve user-added content.| Gem | Purpose | Required? |
|---|---|---|
mcp |
MCP SDK - server, tools, transports | Yes |
prism |
AST parsing (stdlib in Ruby 3.3+) | Yes |
concurrent-ruby |
Thread-safe caches | Yes |
zeitwerk |
Autoloading | Yes |
thor |
CLI framework | Yes |
brakeman |
Security scanning | Optional |
listen |
File watching for live reload | Optional |