stimulus_plumbers_mcp 0.4.5 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +34 -24
- data/lib/stimulus_plumbers/mcp/loaders/aria_loader.rb +31 -0
- data/lib/stimulus_plumbers/mcp/loaders/component_docs_loader.rb +38 -0
- data/lib/stimulus_plumbers/mcp/loaders/component_requirements.rb +41 -0
- data/lib/stimulus_plumbers/mcp/loaders/component_schema_loader.rb +49 -0
- data/lib/stimulus_plumbers/mcp/loaders/component_theme_loader.rb +41 -0
- data/lib/stimulus_plumbers/mcp/loaders/controller_docs_loader.rb +31 -0
- data/lib/stimulus_plumbers/mcp/loaders/controller_schema_loader.rb +49 -0
- data/lib/stimulus_plumbers/mcp/loaders/guide.md +48 -0
- data/lib/stimulus_plumbers/mcp/loaders/guide_loader.rb +40 -4
- data/lib/stimulus_plumbers/mcp/loaders/icons_loader.rb +34 -0
- data/lib/stimulus_plumbers/mcp/loaders/support/docs_table_parser.rb +112 -0
- data/lib/stimulus_plumbers/mcp/loaders/support/gem_vendor_path.rb +18 -0
- data/lib/stimulus_plumbers/mcp/loaders/tailwind_loader.rb +35 -0
- data/lib/stimulus_plumbers/mcp/loaders/versions_loader.rb +110 -0
- data/lib/stimulus_plumbers/mcp/plugins/aria.rb +34 -0
- data/lib/stimulus_plumbers/mcp/plugins/base.rb +40 -31
- data/lib/stimulus_plumbers/mcp/plugins/component_docs.rb +96 -0
- data/lib/stimulus_plumbers/mcp/plugins/component_schema.rb +133 -0
- data/lib/stimulus_plumbers/mcp/plugins/component_theme.rb +90 -0
- data/lib/stimulus_plumbers/mcp/plugins/controller_docs.rb +91 -0
- data/lib/stimulus_plumbers/mcp/plugins/controller_schema.rb +81 -0
- data/lib/stimulus_plumbers/mcp/plugins/guide.rb +70 -16
- data/lib/stimulus_plumbers/mcp/plugins/icons.rb +42 -0
- data/lib/stimulus_plumbers/mcp/plugins/tailwind.rb +69 -45
- data/lib/stimulus_plumbers/mcp/plugins/versions.rb +44 -0
- data/lib/stimulus_plumbers/mcp/server.rb +26 -9
- data/lib/stimulus_plumbers/mcp/version.rb +1 -1
- data/lib/stimulus_plumbers_mcp.rb +31 -11
- metadata +22 -12
- data/lib/stimulus_plumbers/mcp/loaders/component_controller_map.rb +0 -38
- data/lib/stimulus_plumbers/mcp/loaders/docs_loader.rb +0 -129
- data/lib/stimulus_plumbers/mcp/loaders/guide/overview.md +0 -48
- data/lib/stimulus_plumbers/mcp/loaders/schema_loader.rb +0 -45
- data/lib/stimulus_plumbers/mcp/loaders/stimulus_manifest.rb +0 -47
- data/lib/stimulus_plumbers/mcp/loaders/tailwind_theme_loader.rb +0 -29
- data/lib/stimulus_plumbers/mcp/loaders/theme_loader.rb +0 -97
- data/lib/stimulus_plumbers/mcp/plugins/docs.rb +0 -82
- data/lib/stimulus_plumbers/mcp/plugins/schema.rb +0 -110
- data/lib/stimulus_plumbers/mcp/plugins/stimulus.rb +0 -66
- data/lib/stimulus_plumbers/mcp/plugins/theme.rb +0 -66
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33ad1eb09fa4ed8fa1780dffe068f475fc04f3b3e1f179d6cbda211190155dc9
|
|
4
|
+
data.tar.gz: 29fecb05f19a17c3b398d8eb9189167e87cfe2f6f13d2b511472884ed9ebc935
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd757ac9c1c357f3b71c1683816ab497718215ac181e2773af6d02bca45f95114c725e501a3218a71aefa11a4e090d233efdaa085a0a01e7af399745f4082284
|
|
7
|
+
data.tar.gz: 8481e2f13c22dfccbfa0e089442bdec11fe9475c408e4d189567ea4b60b2557aa1f959698c5bc511d428b079f1fbcc5038b327729b4e43b7eb78da8f2261720c
|
data/README.md
CHANGED
|
@@ -18,34 +18,44 @@ gem install stimulus_plumbers_mcp
|
|
|
18
18
|
| URI | Content |
|
|
19
19
|
|-----|---------|
|
|
20
20
|
| `guide://overview` | **Start here** — map of the form/view/stimulus API with pointers to every tool/resource |
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `theme
|
|
30
|
-
| `theme
|
|
31
|
-
| `theme
|
|
32
|
-
| `tailwind
|
|
33
|
-
| `
|
|
21
|
+
| `guide://{name}` | Per-package usage guide: `component` (Rails forms/views), `controller` (plain-JS/non-Rails setup), `tailwind` (Tailwind install/theming), `theme` (custom theme integration contract) |
|
|
22
|
+
| `aria://reference` | WCAG 2.1 AA criteria, keyboard navigation patterns, and per-component ARIA patterns |
|
|
23
|
+
| `component://index` | Index of all component theme keys |
|
|
24
|
+
| `component://icons` | All available icon names |
|
|
25
|
+
| `component://integration` | Map from component key → required Stimulus controller identifiers |
|
|
26
|
+
| `component://{name}/schema` | Params, valid values, defaults + required controllers |
|
|
27
|
+
| `component://{name}/docs` | Full markdown doc with ERB examples |
|
|
28
|
+
| `component://{name}/helper` | Full `sp_` helper surface: keyword options + defaults (grouped by helper signature) + slot methods |
|
|
29
|
+
| `component://theme` | Index of theme-implementable component keys |
|
|
30
|
+
| `component://{name}/theme` | Method name, param signature, return contract |
|
|
31
|
+
| `component://theme/base` | Custom theme authoring guide |
|
|
32
|
+
| `component://tailwind` | Index of Tailwind theme component keys |
|
|
33
|
+
| `component://{name}/tailwind` | Tailwind CSS classes per variant |
|
|
34
|
+
| `controller://index` | Index of Stimulus controller identifiers |
|
|
35
|
+
| `controller://{name}/schema` | Targets, values, outlets, classes for a controller |
|
|
36
|
+
| `controller://docs` | Index of JS controller narrative docs, grouped by controller family |
|
|
37
|
+
| `controller://docs/{name}` | Narrative usage doc for a controller family (plain-JS/Hotwire, no Rails helpers) |
|
|
38
|
+
| `versions://sources` | Resolved version (and resolution path) of each source gem/package |
|
|
34
39
|
|
|
35
40
|
## Tools
|
|
36
41
|
|
|
37
42
|
| Tool | Input | Output |
|
|
38
43
|
|------|-------|--------|
|
|
39
44
|
| `list_components` | — | All component theme keys |
|
|
40
|
-
| `
|
|
41
|
-
| `get_component_schema` | `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
+
| `list_component_docs` | — | Components with markdown docs + helper signatures (the `component://{name}/docs`+`/helper` set) |
|
|
46
|
+
| `get_component_schema` | `name` | Themed params (type/variant/size) + defaults + required controllers |
|
|
47
|
+
| `get_component_helper` | `name` | Full `sp_` helper surface — keyword options grouped by helper signature (incl. `icon_leading`) + slot methods |
|
|
48
|
+
| `get_component_examples` | `name` | ERB code fences from docs |
|
|
49
|
+
| `get_field_as_values` | `builder_method` (`field`/`collection_field`/`choice`) | Valid `as:` values |
|
|
50
|
+
| `get_component_theme` | `name` | Method signature + return contract for custom theme |
|
|
51
|
+
| `get_component_tailwind` | `name` | Tailwind CSS utility classes per variant |
|
|
45
52
|
| `list_controllers` | — | All Stimulus controller identifiers |
|
|
46
|
-
| `get_controller_schema` | `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
53
|
+
| `get_controller_schema` | `name` | Targets, values, outlets, classes |
|
|
54
|
+
| `list_controller_docs` | — | Controller doc families available at `controller://docs/{name}` |
|
|
55
|
+
| `get_controller_docs` | `name` | Narrative usage doc for a controller family |
|
|
56
|
+
| `get_source_versions` | — | Resolved version (and resolution path) of each source |
|
|
57
|
+
| `list_guides` | — | Available per-package guide names (`component`/`controller`/`tailwind`/`theme`) |
|
|
58
|
+
| `get_guide` | `name` | Per-package usage guide content |
|
|
49
59
|
|
|
50
60
|
## IDE Setup
|
|
51
61
|
|
|
@@ -64,7 +74,7 @@ Requires Ruby >= 3.3. No `cwd` required. Pin a specific version by adding it to
|
|
|
64
74
|
|
|
65
75
|
## Stimulus Manifest
|
|
66
76
|
|
|
67
|
-
`
|
|
77
|
+
`controller://` resources require the controller manifest. The server resolves it in order:
|
|
68
78
|
|
|
69
79
|
1. `node_modules/@stimulus-plumbers/controllers/dist/controllers.manifest.json` — npm/yarn/bun projects
|
|
70
80
|
2. `vendor/controllers.manifest.json` inside the installed `stimulus_plumbers` gem — importmaps fallback
|
|
@@ -94,9 +104,9 @@ bundle exec ruby bin/server
|
|
|
94
104
|
bundle exec ruby bin/mcp-query tools # list tools
|
|
95
105
|
bundle exec ruby bin/mcp-query resources # list resources
|
|
96
106
|
bundle exec ruby bin/mcp-query tool list_components # call a tool (no args)
|
|
97
|
-
bundle exec ruby bin/mcp-query tool get_component_schema '{"
|
|
107
|
+
bundle exec ruby bin/mcp-query tool get_component_schema '{"name":"button"}'
|
|
98
108
|
bundle exec ruby bin/mcp-query read guide://overview
|
|
99
|
-
bundle exec ruby bin/mcp-query read
|
|
109
|
+
bundle exec ruby bin/mcp-query read component://form/docs
|
|
100
110
|
```
|
|
101
111
|
|
|
102
112
|
```bash
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
class AriaLoader
|
|
6
|
+
FILENAME = "ARIA.md"
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def call
|
|
10
|
+
path = resolved_path
|
|
11
|
+
path ? File.read(path) : ""
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def resolved_path
|
|
17
|
+
aria_paths.find { |p| File.exist?(p) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def aria_paths
|
|
21
|
+
[
|
|
22
|
+
# 1. Monorepo dev checkout — the root file is the freshest copy while working locally.
|
|
23
|
+
File.expand_path(File.join(__dir__, "../../../../..", FILENAME)),
|
|
24
|
+
# 2. gem exec — vendored into the rails gem at release time (bin/release).
|
|
25
|
+
GemVendorPath.resolve(FILENAME)
|
|
26
|
+
].compact
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
class ComponentDocsLoader
|
|
6
|
+
class << self
|
|
7
|
+
def docs_dir
|
|
8
|
+
@docs_dir ||= resolve_docs_dir
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
Dir[File.join(docs_dir, "*.md")].each_with_object({}) do |path, result|
|
|
13
|
+
name = File.basename(path, ".md").to_sym
|
|
14
|
+
content = File.read(path)
|
|
15
|
+
result[name] = {
|
|
16
|
+
content: content,
|
|
17
|
+
examples: extract_erb_examples(content),
|
|
18
|
+
signature: DocsTableParser.call(content)
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def resolve_docs_dir
|
|
26
|
+
gem_dir = Gem::Specification.find_by_name("stimulus_plumbers").gem_dir
|
|
27
|
+
File.join(gem_dir, "docs/component")
|
|
28
|
+
rescue Gem::MissingSpecError
|
|
29
|
+
File.expand_path("../../../../../stimulus-plumbers-rails/docs/component", __dir__)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def extract_erb_examples(content)
|
|
33
|
+
content.scan(%r{```erb\n(.*?)```}m).map(&:first).map(&:strip)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
class ComponentRequirements
|
|
6
|
+
class << self
|
|
7
|
+
def call
|
|
8
|
+
Components.constants
|
|
9
|
+
.map { |c| Components.const_get(c) }
|
|
10
|
+
.grep(Class)
|
|
11
|
+
.to_h { |klass| [component_key(klass), controllers_for(klass).uniq] }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# Controllers from a component class plus its nested sub-components (e.g. Combobox::Date),
|
|
17
|
+
# keyed to the top-level component. Skips references to sibling components.
|
|
18
|
+
def controllers_for(mod)
|
|
19
|
+
mod.constants(false).flat_map do |const|
|
|
20
|
+
value = mod.const_get(const)
|
|
21
|
+
if const.to_s.end_with?("CONTROLLER")
|
|
22
|
+
Array(value).grep(String).flat_map(&:split)
|
|
23
|
+
elsif nested?(mod, value)
|
|
24
|
+
controllers_for(value)
|
|
25
|
+
else
|
|
26
|
+
[]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def nested?(mod, value)
|
|
32
|
+
value.is_a?(Module) && !value.name.nil? && value.name.start_with?("#{mod.name}::")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def component_key(klass)
|
|
36
|
+
klass.name.demodulize.underscore.to_sym
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
class ComponentSchemaLoader
|
|
6
|
+
# Not derivable from Form::Fields::Renderer's method names (`search:` renders via
|
|
7
|
+
# `render_combobox_typeahead`, but wires up "combobox-dropdown", not
|
|
8
|
+
# "combobox-typeahead") — hand-maintained; keep in sync with form.md's "backed by" notes.
|
|
9
|
+
FIELD_AS_CONTROLLERS = {
|
|
10
|
+
date: "combobox-date",
|
|
11
|
+
time: "combobox-time",
|
|
12
|
+
select: "combobox-dropdown",
|
|
13
|
+
search: "combobox-dropdown",
|
|
14
|
+
collection_select: "combobox-dropdown",
|
|
15
|
+
grouped_collection_select: "combobox-dropdown"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def call
|
|
20
|
+
{
|
|
21
|
+
components: extract_schema,
|
|
22
|
+
field_as: extract_field_as,
|
|
23
|
+
field_as_controllers: FIELD_AS_CONTROLLERS,
|
|
24
|
+
controllers: ComponentRequirements.call
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def extract_schema
|
|
31
|
+
Themes::Base::SCHEMA.transform_values do |param_schema|
|
|
32
|
+
param_schema.transform_values do |meta|
|
|
33
|
+
v = meta[:validate]
|
|
34
|
+
{ default: meta[:default], valid: v.respond_to?(:to_a) ? v.to_a : v.inspect }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def extract_field_as
|
|
40
|
+
{
|
|
41
|
+
field: Form::Fields::Renderer::FIELD.keys,
|
|
42
|
+
collection_field: Form::Fields::Renderer::COLLECTION.keys,
|
|
43
|
+
choice: Form::Fields::Renderer::CHOICE.keys
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
class ComponentThemeLoader
|
|
6
|
+
class << self
|
|
7
|
+
def call
|
|
8
|
+
{
|
|
9
|
+
base_doc: base_doc,
|
|
10
|
+
components: extract_interface
|
|
11
|
+
}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# Same file ComponentDocsLoader already serves at component://theme/docs — avoids a duplicate heredoc.
|
|
17
|
+
def base_doc
|
|
18
|
+
path = File.join(ComponentDocsLoader.docs_dir, "theme.md")
|
|
19
|
+
File.exist?(path) ? File.read(path) : ""
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def extract_interface
|
|
23
|
+
Themes::Base::SCHEMA.each_with_object({}) do |(key, params), result|
|
|
24
|
+
result[key] = {
|
|
25
|
+
method: "#{key}_classes",
|
|
26
|
+
params: params.transform_values { |meta| format_param(meta) },
|
|
27
|
+
returns: "{ classes: String }"
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def format_param(meta)
|
|
33
|
+
valid = meta[:validate]
|
|
34
|
+
entry = { default: meta[:default] }
|
|
35
|
+
entry[:valid] = valid.to_a if valid.respond_to?(:to_a)
|
|
36
|
+
entry
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
class ControllerDocsLoader
|
|
6
|
+
class << self
|
|
7
|
+
def docs_dir
|
|
8
|
+
@docs_dir ||= resolve_docs_dir
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
Dir[File.join(docs_dir, "*.md")].to_h do |path|
|
|
13
|
+
[File.basename(path, ".md").to_sym, File.read(path)]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def resolve_docs_dir
|
|
20
|
+
# 1. Monorepo dev checkout — the JS package's own docs are freshest while working locally.
|
|
21
|
+
dev_path = File.expand_path(File.join(__dir__, "../../../../..", "stimulus-plumbers", "docs", "component"))
|
|
22
|
+
return dev_path if Dir.exist?(dev_path)
|
|
23
|
+
|
|
24
|
+
# 2. gem exec — vendored into the rails gem at release time, under vendor/controller/docs/
|
|
25
|
+
# (mirrors the MCP server's controller:// resource namespace).
|
|
26
|
+
GemVendorPath.resolve("controller", "docs") || dev_path
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
class ControllerSchemaLoader
|
|
6
|
+
MANIFEST_FILENAME = "controllers.manifest.json"
|
|
7
|
+
# Vendored as manifest.json — the "controllers" prefix is redundant once nested
|
|
8
|
+
# under vendor/controller/ (mirrors the MCP server's controller:// namespace).
|
|
9
|
+
VENDOR_FILENAME = "manifest.json"
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def call
|
|
13
|
+
path = resolved_path
|
|
14
|
+
|
|
15
|
+
unless path
|
|
16
|
+
StimulusPlumbers::Logger.warn(
|
|
17
|
+
"controller manifest not found. Tried:\n" \
|
|
18
|
+
"#{manifest_paths.map { |p| " - #{p}" }.join("\n")}\n" \
|
|
19
|
+
"Run `node --run build:manifest` in stimulus-plumbers/ to generate it."
|
|
20
|
+
)
|
|
21
|
+
return {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
JSON.parse(File.read(path))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Reused by VersionsLoader to report which fallback location resolved.
|
|
28
|
+
def resolved_path
|
|
29
|
+
manifest_paths.find { |p| File.exist?(p) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def manifest_paths
|
|
35
|
+
[
|
|
36
|
+
# 1. npm/yarn/bun project — version matches project lockfile
|
|
37
|
+
File.join(Dir.pwd, "node_modules/@stimulus-plumbers/controllers/dist", MANIFEST_FILENAME),
|
|
38
|
+
# 2. importmaps users — vendored in Rails gem at release time
|
|
39
|
+
GemVendorPath.resolve("controller", VENDOR_FILENAME),
|
|
40
|
+
# 3. Monorepo dev fallback
|
|
41
|
+
File.expand_path(
|
|
42
|
+
File.join(__dir__, "../../../../..", "stimulus-plumbers", "dist", MANIFEST_FILENAME)
|
|
43
|
+
)
|
|
44
|
+
].compact
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# stimulus-plumbers — overview
|
|
2
|
+
|
|
3
|
+
Accessible Rails view components plus a themed form builder, backed by Stimulus controllers.
|
|
4
|
+
Start here, then drill into the per-package guides and per-component resources/tools below.
|
|
5
|
+
|
|
6
|
+
## Per-package guides
|
|
7
|
+
- `guide://component` — building forms (`f.field`/`f.collection_field`/`f.choice`) and views
|
|
8
|
+
(`sp_*` helpers) with the Rails gem
|
|
9
|
+
- `guide://controller` — plain-JS / non-Rails setup for `@stimulus-plumbers/controllers`
|
|
10
|
+
- `guide://tailwind` — Tailwind theme install + icons
|
|
11
|
+
- `guide://theme` — implementing a custom theme (subclassing `Themes::Base`)
|
|
12
|
+
|
|
13
|
+
Tools: `list_guides`, `get_guide(name:)`.
|
|
14
|
+
|
|
15
|
+
## Building forms and views
|
|
16
|
+
Valid `as:` values: `get_field_as_values(builder_method:)`. Combobox-backed `as:` values' controller
|
|
17
|
+
identifier: `get_field_as_controller(as:)` — `component://{name}/schema` keys are renderer-level, not
|
|
18
|
+
`as:` values. Component helper surface: `get_component_helper(name)`; themed params:
|
|
19
|
+
`get_component_schema(name)`; ERB examples: `get_component_examples(name)`. List everything with
|
|
20
|
+
`list_components`; `list_component_docs` shows which components have full docs
|
|
21
|
+
(`component://{name}/docs`) and helper signatures (`component://{name}/helper`). Icon options take a
|
|
22
|
+
name from `component://icons` (or `list_icons`). All 77 schema components are queryable via
|
|
23
|
+
`get_component_schema`; only a subset have full docs and ERB examples — use `list_component_docs` to
|
|
24
|
+
see what's covered. Full form builder reference: `component://form/docs`.
|
|
25
|
+
|
|
26
|
+
## Stimulus integration
|
|
27
|
+
Most display components are pure markup; interactive ones (combobox, popover, calendar) emit their
|
|
28
|
+
`data-controller` attributes automatically. Component → required controllers: `component://integration`.
|
|
29
|
+
Controller identifiers and details (targets/values/outlets/classes): `controller://index`,
|
|
30
|
+
`get_controller_schema(id)` / `list_controllers`.
|
|
31
|
+
Narrative docs by controller family: `get_controller_docs(name)` / `controller://docs/{name}`.
|
|
32
|
+
|
|
33
|
+
## How this server is organized
|
|
34
|
+
- **Resources** live under entity namespaces — `component://` (Rails `sp_*` helper surface: schema,
|
|
35
|
+
docs, helper, theme, tailwind, icons facets), `controller://` (plain-JS Stimulus surface: schema,
|
|
36
|
+
docs facets), `guide://` (per-package usage guides) — plus `aria://reference` and `versions://sources`.
|
|
37
|
+
**Tools** (`get_*`, `list_*`) expose the same data for targeted lookups. Use whichever your client prefers.
|
|
38
|
+
- `aria://reference` — WCAG 2.1 AA criteria and component ARIA patterns. For generic ARIA/WCAG reference,
|
|
39
|
+
connect the MDN MCP server too: `claude mcp add --transport http mdn https://mcp.mdn.mozilla.net/`
|
|
40
|
+
- Each source (schema, docs, theme, tailwind, icons, stimulus, stimulus_docs, guide) is read from an
|
|
41
|
+
independently-versioned gem/npm package — `versions://sources` / `get_source_versions` reports what's
|
|
42
|
+
actually resolved, useful if data looks stale or inconsistent with the app code you're generating for.
|
|
43
|
+
`icons` versions with the Tailwind theme gem, not the schema gem, since the bundled icon set is
|
|
44
|
+
Tailwind-specific.
|
|
45
|
+
- **View generation** uses the form builder + `sp_` helper tools/resources above. The `component://theme`
|
|
46
|
+
and `component://tailwind` facets are for *theme authors* implementing a custom theme — skip them when
|
|
47
|
+
generating views.
|
|
48
|
+
- **Errors are uniform:** a not-found tool/resource returns `{ "error": "..." }` (tools also set `isError`).
|
|
@@ -3,12 +3,48 @@
|
|
|
3
3
|
module StimulusPlumbers
|
|
4
4
|
module MCP
|
|
5
5
|
class GuideLoader
|
|
6
|
-
OVERVIEW_PATH = File.expand_path("guide
|
|
6
|
+
OVERVIEW_PATH = File.expand_path("guide.md", __dir__).freeze
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
class << self
|
|
9
|
+
def call
|
|
10
|
+
{
|
|
11
|
+
overview: read_file(OVERVIEW_PATH),
|
|
12
|
+
component: read_file(component_guide_path),
|
|
13
|
+
controller: read_file(controller_guide_path),
|
|
14
|
+
tailwind: read_file(tailwind_guide_path),
|
|
15
|
+
theme: read_file(File.join(ComponentDocsLoader.docs_dir, "theme.md"))
|
|
16
|
+
}
|
|
17
|
+
end
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
# Reused by VersionsLoader to report which fallback location resolved.
|
|
20
|
+
def controller_guide_path
|
|
21
|
+
# 1. Monorepo dev checkout — the JS package's own docs are freshest while working locally.
|
|
22
|
+
dev_path = File.expand_path(File.join(__dir__, "../../../../..", "stimulus-plumbers", "docs", "guide.md"))
|
|
23
|
+
return dev_path if File.exist?(dev_path)
|
|
24
|
+
|
|
25
|
+
# 2. gem exec — vendored into the rails gem at release time, under vendor/controller/guide.md.
|
|
26
|
+
GemVendorPath.resolve("controller", "guide.md")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def read_file(path)
|
|
32
|
+
path && File.exist?(path) ? File.read(path) : ""
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def component_guide_path
|
|
36
|
+
gem_dir = Gem::Specification.find_by_name("stimulus_plumbers").gem_dir
|
|
37
|
+
File.join(gem_dir, "docs/guide.md")
|
|
38
|
+
rescue Gem::MissingSpecError
|
|
39
|
+
File.expand_path("../../../../../stimulus-plumbers-rails/docs/guide.md", __dir__)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def tailwind_guide_path
|
|
43
|
+
gem_dir = Gem::Specification.find_by_name("stimulus_plumbers_tailwind").gem_dir
|
|
44
|
+
File.join(gem_dir, "docs/guide.md")
|
|
45
|
+
rescue Gem::MissingSpecError
|
|
46
|
+
File.expand_path("../../../../../stimulus-plumbers-tailwind/docs/guide.md", __dir__)
|
|
47
|
+
end
|
|
12
48
|
end
|
|
13
49
|
end
|
|
14
50
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
class IconsLoader
|
|
6
|
+
class << self
|
|
7
|
+
def call
|
|
8
|
+
heroicon_dir = Themes::Tailwind::Icons::Heroicon.send(:svg_dir)
|
|
9
|
+
custom_dir = Themes::Tailwind::Icons::Custom.send(:svg_dir)
|
|
10
|
+
|
|
11
|
+
(outline_names(heroicon_dir) + solid_names(heroicon_dir) + custom_names(custom_dir) + alias_names).uniq.sort
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def outline_names(heroicon_dir)
|
|
17
|
+
Dir[File.join(heroicon_dir, "outline", "*.svg")].map { |f| File.basename(f, ".svg") }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def solid_names(heroicon_dir)
|
|
21
|
+
Dir[File.join(heroicon_dir, "solid", "*.svg")].map { |f| "#{File.basename(f, ".svg")}/solid" }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def custom_names(custom_dir)
|
|
25
|
+
Dir[File.join(custom_dir, "*.svg")].map { |f| File.basename(f, ".svg") }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def alias_names
|
|
29
|
+
Themes::Tailwind::Icon::ALIASES.keys
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
class DocsTableParser
|
|
6
|
+
class << self
|
|
7
|
+
def call(content)
|
|
8
|
+
tables = tables_with_headings(content)
|
|
9
|
+
{ helpers: option_helpers(tables), slots: slot_methods(tables) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# A standalone bold label (e.g. `**Time**`) between `#` headings is treated as a
|
|
13
|
+
# sub-heading, since some docs use one to introduce a table scoped to part of a
|
|
14
|
+
# section. It only labels the single table immediately following it — a fenced
|
|
15
|
+
# code block or a second table both clear it, so it can't leak onto later tables.
|
|
16
|
+
def tables_with_headings(content)
|
|
17
|
+
state = { heading: nil, subheading: nil, fenced: false, buffer: [], tables: [] }
|
|
18
|
+
content.each_line { |line| scan_line(line, state) }
|
|
19
|
+
flush_table(state)
|
|
20
|
+
state[:tables]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def option_helpers(tables)
|
|
26
|
+
tables.select { |t| t[:header].first == "Option" }
|
|
27
|
+
.filter_map do |t|
|
|
28
|
+
options = t[:rows].map { |r| option_row(r) }
|
|
29
|
+
{ signature: t[:heading], options: options } unless options.empty?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def slot_methods(tables)
|
|
34
|
+
tables.select { |t| t[:header].first == "Slot method" }
|
|
35
|
+
.flat_map { |t| t[:rows].map { |r| slot_row(r) } }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def option_row(cells)
|
|
39
|
+
{ option: clean(cells[0]), default: clean(cells[1]), description: cells[2].to_s }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def slot_row(cells)
|
|
43
|
+
slot = clean(cells[0])
|
|
44
|
+
description = cells[1].to_s
|
|
45
|
+
{ slot: slot, description: description, block: block_required?(slot, description) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def block_required?(slot, description)
|
|
49
|
+
slot.include?("{") || description.match?(%r{block required}i)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def scan_line(line, state)
|
|
53
|
+
if line.start_with?("```")
|
|
54
|
+
toggle_fence(state)
|
|
55
|
+
elsif state[:fenced]
|
|
56
|
+
nil
|
|
57
|
+
elsif (heading = line[%r{\A#+\s+(.+)}, 1])
|
|
58
|
+
set_heading(state, heading)
|
|
59
|
+
elsif (subheading = line[%r{\A\*\*([^*]+)\*\*}, 1])
|
|
60
|
+
set_subheading(state, subheading)
|
|
61
|
+
elsif line.lstrip.start_with?("|")
|
|
62
|
+
state[:buffer] << line
|
|
63
|
+
else
|
|
64
|
+
flush_table(state)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def toggle_fence(state)
|
|
69
|
+
flush_table(state)
|
|
70
|
+
state[:subheading] = nil
|
|
71
|
+
state[:fenced] = !state[:fenced]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def set_heading(state, heading)
|
|
75
|
+
flush_table(state)
|
|
76
|
+
state[:heading] = clean(heading)
|
|
77
|
+
state[:subheading] = nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def set_subheading(state, subheading)
|
|
81
|
+
flush_table(state)
|
|
82
|
+
state[:subheading] = clean(subheading)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def flush_table(state)
|
|
86
|
+
return if state[:buffer].empty?
|
|
87
|
+
|
|
88
|
+
heading = [state[:heading], state[:subheading]].compact.join(" — ")
|
|
89
|
+
state[:tables] << build_table(state[:buffer]).merge(heading: heading)
|
|
90
|
+
state[:buffer] = []
|
|
91
|
+
state[:subheading] = nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_table(lines)
|
|
95
|
+
rows = lines.map { |l| split_row(l) }.reject { |cells| cells.all? { |c| c.match?(%r{\A:?-+:?\z}) } }
|
|
96
|
+
{ header: rows.first, rows: rows.drop(1) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Split a markdown table row, honouring escaped pipes (`\|`) inside cells.
|
|
100
|
+
def split_row(line)
|
|
101
|
+
line.strip.delete_prefix("|").delete_suffix("|")
|
|
102
|
+
.split(%r{(?<!\\)\|})
|
|
103
|
+
.map { |c| c.gsub('\|', "|").strip }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def clean(cell)
|
|
107
|
+
cell.to_s.gsub(%r{[`*]}, "").sub(%r{:\z}, "").strip
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
# Path under the stimulus_plumbers gem's vendor/ dir (populated by bin/release),
|
|
6
|
+
# used by loaders as their `gem exec` fallback when there's no monorepo checkout.
|
|
7
|
+
module GemVendorPath
|
|
8
|
+
GEM_NAME = "stimulus_plumbers"
|
|
9
|
+
|
|
10
|
+
def self.resolve(*relative)
|
|
11
|
+
gem_dir = Gem::Specification.find_by_name(GEM_NAME).gem_dir
|
|
12
|
+
File.join(gem_dir, "vendor", *relative)
|
|
13
|
+
rescue Gem::MissingSpecError
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|