Editors
Poetry ships no bespoke extension. It plugs into any MCP-capable editor through a one-line config, and one generator writes those configs plus component snippets for you.
One generator
poetry:editor writes an MCP config for each editor, a
snippet file generated from the component registry, and a check hook
that lints every template an agent edits. The config writes are
upserts: a file keeps its other servers and hooks, one that already has
Poetry is left alone, and a JSONC file Poetry cannot parse is reported,
never clobbered.
bin/rails g poetry:editor
| Editor | What it writes | Gets |
|---|---|---|
| Claude Code | .mcp.json + .claude/settings.json + bin/poetry-check-hook | MCP + check hook |
| Cursor | .cursor/mcp.json + .cursor/hooks.json | MCP + snippets + check hook |
| VS Code | .vscode/mcp.json + .vscode/poetry.code-snippets | MCP + snippets |
| Windsurf | reads the .vscode snippets | MCP (paste) + snippets |
| Zed / RubyMine / Neovim | a printed paste-block | MCP |
| Any Herb-aware editor | .herb.yml | linter + language server config |
The MCP server
Poetry's agent surface is a standard stdio MCP server
(bundle exec poetry-agent, from the poetry-agent gem): boot-free, read-only, no SDK.
Its ten tools teach an agent the catalog and gate its work. Call
compose first, build from primitives, run
check last, and take a PASS verdict rather than an eyeball.
// .mcp.json (Claude Code) / .cursor/mcp.json (Cursor)
{ "mcpServers": { "poetry": { "command": "bundle", "args": ["exec", "poetry-agent"] } } }
// .vscode/mcp.json (VS Code)
{ "servers": { "poetry": { "type": "stdio", "command": "bundle", "args": ["exec", "poetry-agent"] } } }
// Zed settings.json
"context_servers": { "poetry": { "command": { "path": "bundle", "args": ["exec", "poetry-agent"] } } }
RubyMine 2025.2+ is already an MCP client: Settings, then Tools, AI
Assistant, Model Context Protocol; add a stdio server running
bundle exec poetry-agent or import the
.mcp.json.
The check hook
A rule in a skill file competes with everything else the agent is
holding; a hook does not. poetry:editor writes
bin/poetry-check-hook and registers it so that every
template an agent edits under app/ is linted the moment
the edit lands: Claude Code's PostToolUse hook on Edit and
Write, Cursor's postToolUse on Write. Errors go straight
back to the agent as feedback it has to act on, warnings arrive as
context, and a clean template costs one Rails boot and says nothing.
The script is plain Ruby with no dependencies and runs
bin/rails poetry:check on the one file, so it sees exactly
what the rake task sees. Anything that is not an app template is
ignored without booting Rails, and a check that cannot run at all fails
open.
Claude Code reads hooks at session start: after generating, start a new
session (or check /hooks) before expecting feedback. The
Cursor entry is best-effort; its edit payload is read leniently and the
hook stays silent when it finds no path.
// .claude/settings.json (Claude Code) - written by poetry:editor
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write|MultiEdit",
"hooks": [ { "type": "command", "command": "bin/poetry-check-hook", "timeout": 120 } ] } ] } }
// .cursor/hooks.json (Cursor)
{ "version": 1, "hooks": { "postToolUse": [ { "command": "bin/poetry-check-hook", "matcher": "Write" } ] } }
Snippets
.vscode/poetry.code-snippets holds one snippet per
poetry_* helper, generated from the committed registry so
it never drifts from the shipped catalog. Type a helper name and the
snippet expands with the real variant and
size values as a choice list, a block form for components
that compose through slots, and inline for atoms. One file works in VS
Code, Cursor, and Windsurf.
HTML+ERB tooling
Rails 8.2 made Herb its ERB
engine, and Poetry's templates are gated to parse and compile under
it, so the whole Herb toolchain works on a Poetry app: the language
server (VS Code's Herb LSP extension, built into Zed's Ruby
extension, @herb-tools/language-server for Neovim), the
linter, and the render-graph check. Stimulus LSP runs on the same
language service and reads data-controller in ERB and
tag.div data: { controller: } alike. It resolves
controllers from your app, not from a gem, so poetry:editor
lists Poetry's identifiers in .stimulus-lsp/config.json
and a hand-written poetry--core--sheet#close is never an
"invalid controller"; bin/rails poetry:check is what
validates those, data: keywords on Poetry helpers included
(the Stimulus guide covers composing
your controllers with Poetry's).
poetry:editor writes the one config all of it reads,
and keeps yours if you have one. Two rules start off: the linter
still misreads slot setters on a builder a Poetry helper yields
(card.with_title inside poetry_card do |card|)
and brace-form setters; both are tracked upstream and the pinned
version means an upgrade never switches rules on behind you.
# .herb.yml - written by poetry:editor (kept if you already have one)
version: 0.10.3
framework: actionview
linter:
enabled: true
rules:
erb-no-unused-expressions: { enabled: false } # marcoroth/herb#2426
actionview-no-silent-helper: { enabled: false } # marcoroth/herb#2340
npx --yes @herb-tools/linter app # lint (Node); bundle exec herb lint delegates to it
bundle exec herb actionview check . # every render resolves, every partial is reachable
The formatter is still an upstream preview; Poetry's own templates are not run through it, because it rewraps the inline one-liners the components depend on. Run it on your views if you like the output.
Design tools
With the Poetry MCP wired alongside a Figma or Paper MCP, an agent can
translate a design into Poetry components; the poetry-design
skill's figma and paper references are the
rulebook. Design tokens come in through the same AA-gated importer a
DESIGN.md uses:
bin/rails poetry:figma:import[variables.json] # a Figma variables export
bin/rails poetry:paper:import[paper-theme.css] # a Paper "Copy theme"
A swatch that fails WCAG AA is dropped and reported with the nearest passing value, never shipped.