Midwest MCP Server
Overview
Midwest ships an MCP (Model Context Protocol) server as a bundled executable. It exposes your component library to AI agents — Claude Code, Cursor, Windsurf, or any MCP-compatible client — so they can discover components, look up APIs, search by capability, and pull usage examples without you pasting documentation into a prompt.
The server runs over stdio transport and requires no network configuration. It parses your installed Midwest components at startup and indexes them in memory.
Setup
1. Verify the executable is available
The midwest_mcp executable is included with the gem. After installing or updating Midwest, confirm it's accessible:
bundle exec midwest_mcp --helpIf you installed Midwest globally, the executable is available directly:
midwest_mcp2. Configure your MCP client
Each AI tool has its own configuration format. Below are examples for the most common clients.
Claude Code
Add a .mcp.json file to your project root:
{ "mcpServers": { "midwest": { "command": "bundle", "args": ["exec", "midwest_mcp"] } }}Cursor
Add to your .cursor/mcp.json:
{ "mcpServers": { "midwest": { "command": "bundle", "args": ["exec", "midwest_mcp"] } }}VS Code (Copilot)
Add to your .vscode/mcp.json:
{ "servers": { "midwest": { "command": "bundle", "args": ["exec", "midwest_mcp"] } }}Windsurf
Add to your ~/.codeium/windsurf/mcp_config.json:
{ "mcpServers": { "midwest": { "command": "bundle", "args": ["exec", "midwest_mcp"] } }}3. Restart your AI tool
After adding the configuration, restart the client (or reload MCP servers if the tool supports it). The server starts automatically when the client connects.
How it works
- The client launches
midwest_mcpas a child process - The server locates your Midwest gem installation and scans all component files under
app/components/midwest/ - It parses each component to extract options, slots, constants, public methods, and descriptions
- It parses Lookbook preview files for usage examples and parameter annotations
- It parses Stimulus TypeScript controllers to extract values, targets, and actions
- It parses the view helper module and form builder to index helper methods and form field mappings
- A search index is built over the parsed metadata
- The server listens on stdin/stdout using the MCP stdio transport protocol
- When the AI agent calls a tool, the server queries its in-memory indexes and returns structured JSON
The server is read-only — it never modifies files or makes network requests.
Available tools
The MCP server exposes eight tools to connected AI agents. Each section below shows the tool's parameters, an example invocation as the AI agent sends it over the protocol, and the response the agent receives.
list_components
Lists all available Midwest components with basic metadata.
| Parameter | Type | Required | Description |
|---|---|---|---|
category |
string |
no | Filter by "elements" or "form" |
Returns each component's name, class name, category, helper method name, option/slot counts, and description.
Invocation:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_components", "arguments": {} }}Pass "arguments": { "category": "form" } to return only form components.
Response:
{ "components": [ { "name": "button_component", "class_name": "Midwest::ButtonComponent", "category": "elements", "helper": "midwest_button", "options_count": 18, "slots_count": 1, "description": null }, { "name": "dialog_component", "class_name": "Midwest::DialogComponent", "category": "elements", "helper": "midwest_dialog", "options_count": 8, "slots_count": 0, "description": null }, { "name": "form/input_component", "class_name": "Midwest::Form::InputComponent", "category": "form", "helper": "midwest_form_input", "options_count": 25, "slots_count": 0, "description": null } ], "count": 75}The helper field is the view helper method you call in ERB (e.g. <%= midwest_button "Save" %>). Use options_count and slots_count to gauge a component's complexity before diving into its full API.
get_component_api
Returns the full API surface for a specific component — options with defaults, slots with render type, constants with values, and public methods.
| Parameter | Type | Required | Description |
|---|---|---|---|
component |
string |
yes | Component name (e.g. "button_component", "form/input_component", or "button") |
The tool accepts flexible name formats: "button", "button_component", "form/input_component", or "Midwest::ButtonComponent" all resolve correctly.
Invocation:
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "get_component_api", "arguments": { "component": "button" } }}Response:
{ "name": "button_component", "class_name": "Midwest::ButtonComponent", "file_path": "app/components/midwest/button_component.rb", "description": null, "category": "elements", "helper": "midwest_button", "options": [ { "name": "as", "default": "link" }, { "name": "size", "default": "default" }, { "name": "variant", "default": "default" }, { "name": "fill", "default": "false" }, { "name": "pill", "default": "false" }, { "name": "icon", "default": null }, { "name": "icon_position", "default": "start" }, { "name": "disabled", "default": "false" }, { "name": "tooltip", "default": null }, { "name": "loading", "default": "false" } ], "slots": [ { "name": "badge", "renders": "one", "component": null } ], "constants": [ { "name": "AS_OPTIONS", "values": ["link", "button", "submit", "reset", "span"] }, { "name": "VARIANT_OPTIONS", "values": ["default", "outline", "ghost", "tab", "fab"] }, { "name": "SIZE_OPTIONS", "values": ["large", "default", "small", "tiny"] } ], "public_methods": [ { "name": "element_classes" }, { "name": "button_tag" }, { "name": "button_options" } ]}The constants array lists allowed values for enum-like options — an agent can read VARIANT_OPTIONS to know which values are valid for variant:. The options array includes each option's default so the agent only needs to pass values that differ.
For components with Stimulus controllers, the response includes a stimulus key with the controller's values, targets, and actions:
{ "stimulus": { "controller_id": "midwest-dropdown", "values": [ { "name": "computed", "type": "Boolean", "default": "false" }, { "name": "supported", "type": "Boolean", "default": "true" }, { "name": "align", "type": "String", "default": "right" } ], "targets": [], "actions": ["computedCB"] }}This lets agents set data attributes correctly when customizing interactive behavior (e.g. data-midwest-dropdown-align-value="left"). Components without Stimulus controllers omit this key entirely.
If the component is not found, the response contains an error key instead:
{ "error": "Component 'nonexistent' not found" }search_components
Full-text search across component names, descriptions, options, slots, methods, and constants. Results are ranked by relevance.
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string |
yes | Search term (e.g. "dropdown", "form input", "icon") |
category |
string |
no | Filter by "elements" or "form" |
The search scores matches in: class name (10 pts), description (5 pts), options (3 pts each), slots (3 pts each), methods (2 pts each), and constants (1 pt each). Multi-word queries are split and scored independently.
Invocation:
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "search_components", "arguments": { "query": "dropdown" } }}Pass "category": "form" in arguments to restrict results to form components only.
Response:
{ "query": "dropdown", "results": [ { "component": "dropdown_menu_component", "class_name": "Midwest::DropdownMenuComponent", "category": "elements", "helper": "midwest_dropdown_menu", "relevance": 13.0, "description": null }, { "component": "dropdown_component", "class_name": "Midwest::DropdownComponent", "category": "elements", "helper": "midwest_dropdown", "relevance": 12.0, "description": null }, { "component": "dropdown_item_component", "class_name": "Midwest::DropdownItemComponent", "category": "elements", "helper": "midwest_dropdown_item", "relevance": 12.0, "description": null } ], "count": 7}Results are sorted by relevance descending. The top result is always the closest match. Use this tool when you don't know the exact component name — it will surface related components across the entire library.
get_component_examples
Retrieves usage examples from Lookbook preview files for a given component, including parameter annotations, descriptions, and source file paths.
| Parameter | Type | Required | Description |
|---|---|---|---|
component |
string |
yes | Component name (e.g. "button_component", "form/input_component", or "button") |
Invocation:
{ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "get_component_examples", "arguments": { "component": "button" } }}Response:
{ "component": "button_component", "class_name": "Midwest::ButtonComponent", "examples": [ { "name": "default", "description": "This is the minimum code needed to make a tag.", "source": "preview", "file": "demo/app/previews/elements/button_component_preview.rb" }, { "name": "playground", "description": "which can be edited live in the Lookbook UI", "source": "preview", "file": "demo/app/previews/elements/button_component_preview.rb", "parameters": [ { "name": "label", "type": "text", "description": "The button label" }, { "name": "variant", "type": "select", "description": "Visual style variant", "choices": ["default", "outline", "ghost"] }, { "name": "size", "type": "select", "description": "Button size", "choices": ["large", "default", "small", "tiny"] }, { "name": "icon", "type": "select" } ] }, { "name": "fab", "description": "Intended to float above content; position it with fixed or absolute in context.", "source": "preview", "file": "demo/app/previews/elements/button_component_preview.rb" } ], "count": 6}Each example maps to a method in the Lookbook preview file. The file path tells you where to read the source code for that example. The parameters array (present on interactive examples like playground) lists the knobs available — the choices array shows allowed values, which correspond to the component's constants.
Use this tool after get_component_api to see how a component is actually used in practice.
get_helper_methods
Lists all view helper methods provided by Midwest, including both standard component helpers and special helpers like midwest_form_with and midwest_table_from.
| Parameter | Type | Required | Description |
|---|---|---|---|
helper |
string |
no | Filter to a single helper by name (e.g. "midwest_button" or "midwest_table_from") |
Without a filter, returns all standard helpers (component → helper mappings) and special helpers (with signatures and descriptions).
Invocation:
{ "jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": { "name": "get_helper_methods", "arguments": {} }}Response:
{ "standard_helpers": [ { "helper": "midwest_button", "component": "Midwest::ButtonComponent" }, { "helper": "midwest_tabs", "component": "Midwest::TabsComponent" }, { "helper": "midwest_form_input", "component": "Midwest::Form::InputComponent" } ], "special_helpers": [ { "method": "midwest_table_from", "signature": "midwest_table_from(presenter, **html_options, &extra_columns)", "description": "Renders a Midwest::TableComponent with columns, sort state, and pagination fully configured from a Midwest::TablePresenter instance." }, { "method": "midwest_form_with", "signature": "midwest_form_with(builder: Midwest::FormBuilder, **args, &)" }, { "method": "midwest_colors", "signature": "midwest_colors" } ], "count": 86}With a helper filter, the response returns just that helper's details:
{ "helper": "midwest_button", "component": "Midwest::ButtonComponent" }Use this tool to discover which helper method to call in ERB for a given component, or to explore the special helpers that don't map 1:1 to a component.
get_form_builder_info
Documents the Midwest form builder — which Rails form methods are available and which Midwest components they render.
| Parameter | Type | Required | Description |
|---|---|---|---|
method |
string |
no | Filter to a single form method (e.g. "email_field", "select") |
Without a filter, returns all form builder methods with their signatures, target components, and any implicit options the builder sets automatically.
Invocation:
{ "jsonrpc": "2.0", "id": 6, "method": "tools/call", "params": { "name": "get_form_builder_info", "arguments": { "method": "email_field" } }}Response (single method):
{ "method": "email_field", "signature": "email_field(method, **options, &)", "component": "Midwest::Form::InputComponent", "implicit_options": { "type": "email" }}The implicit_options hash shows options the form builder sets for you — here, email_field automatically sets type: "email" on the underlying InputComponent, so you don't need to pass it yourself.
Response (all methods):
{ "form_builder_class": "Midwest::FormBuilder", "extends": "ActionView::Helpers::FormBuilder", "usage": "midwest_form_with do |f| ... end", "methods": [ { "method": "text_field", "signature": "text_field(method, **options, &)", "component": "Midwest::Form::InputComponent" }, { "method": "email_field", "signature": "email_field(method, **options, &)", "component": "Midwest::Form::InputComponent", "implicit_options": { "type": "email" } }, { "method": "select", "signature": "select(method, choices = nil, options = {}, **html_options, &)", "component": "Midwest::Form::SelectComponent" } ], "count": 48}Use this tool when building forms with midwest_form_with to know exactly which methods are available and what they render.
get_component_relationships
Shows how a component relates to other components in the library — which components it renders as children via slots, which parent components render it, which view helpers reference it, and which form builder methods create it.
| Parameter | Type | Required | Description |
|---|---|---|---|
component |
string |
yes | Component name (e.g. "carousel", "form/input_component") |
Invocation:
{ "jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": { "name": "get_component_relationships", "arguments": { "component": "carousel" } }}Response:
{ "component": "carousel_component", "class_name": "Midwest::CarouselComponent", "renders_children": [ { "slot": "slides", "kind": "many", "component": "Midwest::SlideComponent" } ], "rendered_by_parents": [], "view_helpers": ["midwest_carousel"], "form_builder_methods": []}The renders_children array shows slot-based composition — here, CarouselComponent accepts many SlideComponent instances via its slides slot. The rendered_by_parents array is the reverse: it shows which parent components render this component in one of their slots.
Use this tool to understand component composition patterns and discover related components.
validate_component_usage
Validates a proposed component usage against the parsed API. Checks that option names are recognized, option values match allowed constants, and slot names exist on the component.
| Parameter | Type | Required | Description |
|---|---|---|---|
component |
string |
yes | Component name |
options |
object |
no | Hash of option names to values to validate |
slots |
array |
no | Array of slot names being used |
Invocation:
{ "jsonrpc": "2.0", "id": 8, "method": "tools/call", "params": { "name": "validate_component_usage", "arguments": { "component": "button", "options": { "colour": "red", "variant": "primary" }, "slots": ["nonexistent_slot"] } }}Response:
{ "component": "button_component", "class_name": "Midwest::ButtonComponent", "valid": false, "errors": [ { "type": "unknown_option", "option": "colour", "message": "Unknown option 'colour'." }, { "type": "invalid_value", "option": "variant", "value": "primary", "message": "Invalid value 'primary' for option 'variant'. Allowed values: default, outline, ghost, tab, tab-vertical, fab, mobile-nav", "allowed_values": ["default", "outline", "ghost", "tab", "tab-vertical", "fab", "mobile-nav"] }, { "type": "unknown_slot", "slot": "nonexistent_slot", "message": "Unknown slot 'nonexistent_slot'." } ], "warnings": [ { "type": "unused_slot", "slot": "badge", "kind": "one", "message": "Component has slot 'badge' (one) that is not being used" } ]}When all inputs are valid, the response returns "valid": true with an empty errors array. The warnings array notes available slots that aren't being used — these are informational, not errors.
This tool is especially useful for agents to self-check generated code before presenting it. An agent can validate its proposed helper call and fix any issues before the user sees the output.
Using the --root flag
By default, the server auto-detects the Midwest gem location — either from a local midwest.gemspec (development) or from the installed gem path. You can override this with --root:
bundle exec midwest_mcp --root /path/to/midwestThis is primarily useful during gem development or when running the server against a checkout that isn't the installed gem.
To pass --root through an MCP client configuration:
{ "mcpServers": { "midwest": { "command": "bundle", "args": ["exec", "midwest_mcp", "--root", "/path/to/midwest"] } }}Tips for effective use
- Ask the agent to search first. A prompt like "which Midwest component handles dropdown menus?" will trigger
search_componentsand return ranked matches — better than guessing the component name. - Request the API before generating code. The agent can call
get_component_apito learn exact option names, defaults, and Stimulus controller data — producing correct helper calls on the first try. - Use examples for context.
get_component_examplesgives the agent real preview code to reference, which improves the quality of generated markup. - Scope with categories. If you're working on a form, tell the agent to filter by the
"form"category to narrow results. - Check helper methods. When unsure which helper to use,
get_helper_methodsmaps every component to its ERB helper name. - Use validation as a safety net. After generating component code, agents can call
validate_component_usageto catch typos and invalid values before presenting the result. - Explore relationships.
get_component_relationshipsreveals composition patterns — which components nest inside others — so agents can generate complete, nested markup.