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
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StimulusPlumbers
|
|
4
|
-
module MCP
|
|
5
|
-
class DocsLoader
|
|
6
|
-
def self.docs_dir
|
|
7
|
-
@docs_dir ||= resolve_docs_dir
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def self.resolve_docs_dir
|
|
11
|
-
gem_dir = Gem::Specification.find_by_name("stimulus_plumbers").gem_dir
|
|
12
|
-
File.join(gem_dir, "docs/component")
|
|
13
|
-
rescue Gem::MissingSpecError
|
|
14
|
-
File.expand_path("../../../../../stimulus-plumbers-rails/docs/component", __dir__)
|
|
15
|
-
end
|
|
16
|
-
private_class_method :resolve_docs_dir
|
|
17
|
-
|
|
18
|
-
def self.call
|
|
19
|
-
Dir[File.join(docs_dir, "*.md")].each_with_object({}) do |path, result|
|
|
20
|
-
name = File.basename(path, ".md").to_sym
|
|
21
|
-
content = File.read(path)
|
|
22
|
-
result[name] = {
|
|
23
|
-
content: content,
|
|
24
|
-
examples: extract_erb_examples(content),
|
|
25
|
-
signature: extract_signature(content)
|
|
26
|
-
}
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def self.extract_erb_examples(content)
|
|
31
|
-
content.scan(%r{```erb\n(.*?)```}m).map(&:first).map(&:strip)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Parse `| Option |` / `| Slot method |` doc tables into the helper surface.
|
|
35
|
-
# Options are grouped under the heading (sub-helper signature) above them.
|
|
36
|
-
def self.extract_signature(content)
|
|
37
|
-
tables = tables_with_headings(content)
|
|
38
|
-
{ helpers: option_helpers(tables), slots: slot_methods(tables) }
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def self.option_helpers(tables)
|
|
42
|
-
tables.select { |t| t[:header].first == "Option" }
|
|
43
|
-
.filter_map do |t|
|
|
44
|
-
options = t[:rows].map { |r| option_row(r) }
|
|
45
|
-
{ signature: t[:heading], options: options } unless options.empty?
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def self.slot_methods(tables)
|
|
50
|
-
tables.select { |t| t[:header].first == "Slot method" }
|
|
51
|
-
.flat_map { |t| t[:rows].map { |r| slot_row(r) } }
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def self.option_row(cells)
|
|
55
|
-
{ option: clean(cells[0]), default: clean(cells[1]), description: cells[2].to_s }
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def self.slot_row(cells)
|
|
59
|
-
slot = clean(cells[0])
|
|
60
|
-
description = cells[1].to_s
|
|
61
|
-
{ slot: slot, description: description, block: block_required?(slot, description) }
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def self.block_required?(slot, description)
|
|
65
|
-
slot.include?("{") || description.match?(%r{block required}i)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Tag each table with the heading above it; skip fenced code (so ```ruby
|
|
69
|
-
# comments aren't read as headings).
|
|
70
|
-
def self.tables_with_headings(content)
|
|
71
|
-
state = { heading: nil, fenced: false, buffer: [], tables: [] }
|
|
72
|
-
content.each_line { |line| scan_line(line, state) }
|
|
73
|
-
flush_table(state)
|
|
74
|
-
state[:tables]
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def self.scan_line(line, state)
|
|
78
|
-
if line.start_with?("```")
|
|
79
|
-
flush_table(state)
|
|
80
|
-
state[:fenced] = !state[:fenced]
|
|
81
|
-
elsif state[:fenced]
|
|
82
|
-
nil
|
|
83
|
-
elsif (heading = line[%r{\A#+\s+(.+)}, 1])
|
|
84
|
-
flush_table(state)
|
|
85
|
-
state[:heading] = clean(heading)
|
|
86
|
-
elsif line.lstrip.start_with?("|")
|
|
87
|
-
state[:buffer] << line
|
|
88
|
-
else
|
|
89
|
-
flush_table(state)
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def self.flush_table(state)
|
|
94
|
-
return if state[:buffer].empty?
|
|
95
|
-
|
|
96
|
-
state[:tables] << build_table(state[:buffer]).merge(heading: state[:heading])
|
|
97
|
-
state[:buffer] = []
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def self.build_table(lines)
|
|
101
|
-
rows = lines.map { |l| split_row(l) }.reject { |cells| cells.all? { |c| c.match?(%r{\A:?-+:?\z}) } }
|
|
102
|
-
{ header: rows.first, rows: rows.drop(1) }
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Split a markdown table row, honouring escaped pipes (`\|`) inside cells.
|
|
106
|
-
def self.split_row(line)
|
|
107
|
-
line.strip.delete_prefix("|").delete_suffix("|")
|
|
108
|
-
.split(%r{(?<!\\)\|})
|
|
109
|
-
.map { |c| c.gsub('\|', "|").strip }
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def self.clean(cell)
|
|
113
|
-
cell.to_s.gsub(%r{[`*]}, "").sub(%r{:\z}, "").strip
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
private_class_method :option_helpers,
|
|
117
|
-
:slot_methods,
|
|
118
|
-
:option_row,
|
|
119
|
-
:slot_row,
|
|
120
|
-
:block_required?,
|
|
121
|
-
:tables_with_headings,
|
|
122
|
-
:scan_line,
|
|
123
|
-
:flush_table,
|
|
124
|
-
:build_table,
|
|
125
|
-
:split_row,
|
|
126
|
-
:clean
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
@@ -1,48 +0,0 @@
|
|
|
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-component resources/tools below.
|
|
5
|
-
|
|
6
|
-
## Tailwind theme setup
|
|
7
|
-
The `stimulus_plumbers_tailwind` gem provides themed CSS classes for all components. After adding it to
|
|
8
|
-
your Gemfile, run the install generator once:
|
|
9
|
-
bin/rails generate stimulus_plumbers_tailwind:install
|
|
10
|
-
This injects an `@source` directive into your Tailwind CSS entry file so component classes are included
|
|
11
|
-
in the compiled output. The generator checks `app/assets/stylesheets/application.tailwind.css`,
|
|
12
|
-
`app/assets/stylesheets/application.css`, and `app/javascript/entrypoints/application.css` in that
|
|
13
|
-
order. Override with `TAILWIND_CSS_FILE=/path/to/entry.css`. The path updates automatically on
|
|
14
|
-
`assets:precompile` and `tailwindcss:build` — no re-run needed after `bundle update`.
|
|
15
|
-
|
|
16
|
-
## Building forms
|
|
17
|
-
Use `StimulusPlumbers::Form::Builder` (set `config.action_view.default_form_builder`, or pass
|
|
18
|
-
`builder:` to `form_with`). Two levels:
|
|
19
|
-
- **Level 2 — recommended.** Full accessible field (label + input + hint + error):
|
|
20
|
-
`f.field(attr, as:)`, `f.collection_field(attr, as:, collection:, ...)`, `f.choice(attr, as:)`.
|
|
21
|
-
Valid `as:` values come from `get_field_types` (pass `builder_method: "field"`, `"collection_field"`, or `"choice"`).
|
|
22
|
-
- **Level 1.** Native helper overrides (`f.text_field`, `f.select`, `f.check_box`, ...) render
|
|
23
|
-
only the themed input element — use when you control the surrounding markup.
|
|
24
|
-
Submit with `f.submit` (themed button). Full form reference + examples: read `docs://components/form`.
|
|
25
|
-
|
|
26
|
-
## Building views
|
|
27
|
-
Render components with `sp_*` helpers (`sp_button`, `sp_button_group`, `sp_card`, `sp_list`,
|
|
28
|
-
`sp_link`, `sp_avatar`, `sp_divider`, `sp_icon`, `sp_popover`). For any component:
|
|
29
|
-
- `get_helper_signature(name)` — full helper surface: keyword options (incl. `icon_leading`) + slot methods
|
|
30
|
-
- `get_component_schema(name)` — themed params (type/variant/size) with valid values + defaults
|
|
31
|
-
- `get_erb_examples(name)` — runnable ERB snippets
|
|
32
|
-
List everything with `list_components`; `list_docs` shows which components have full docs
|
|
33
|
-
(`docs://components/{name}`) and helper signatures (`helper://components/{name}`).
|
|
34
|
-
Icon options take a name from `schema://icons`.
|
|
35
|
-
All 77 schema components are queryable via `get_component_schema`; only a subset have full docs and ERB examples — use `list_docs` to see what's covered.
|
|
36
|
-
|
|
37
|
-
## Stimulus wiring
|
|
38
|
-
Most display components are pure markup; interactive ones (combobox, popover, calendar) emit their
|
|
39
|
-
`data-controller` wiring automatically. Component → required controllers: `schema://stimulus`.
|
|
40
|
-
Controller details (targets/values/outlets/classes): `get_controller_schema(id)` / `list_controllers`.
|
|
41
|
-
|
|
42
|
-
## How this server is organized
|
|
43
|
-
- **Resources** (`schema://`, `docs://`, `helper://`, `theme://`, `tailwind://`, `guide://`) are for
|
|
44
|
-
browsing/pulling context; **tools** (`get_*`, `list_*`) are for targeted lookups. They expose the
|
|
45
|
-
same data two ways — use whichever your client prefers.
|
|
46
|
-
- **View generation** uses the form builder + `sp_` helper tools/resources above. The `theme://` and
|
|
47
|
-
`tailwind://` surfaces are for *theme authors* implementing a custom theme — skip them when generating views.
|
|
48
|
-
- **Errors are uniform:** a not-found tool/resource returns `{ "error": "..." }` (tools also set `isError`).
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StimulusPlumbers
|
|
4
|
-
module MCP
|
|
5
|
-
class SchemaLoader
|
|
6
|
-
def self.call
|
|
7
|
-
{
|
|
8
|
-
components: extract_schema,
|
|
9
|
-
field_as: extract_field_as,
|
|
10
|
-
icons: extract_icons,
|
|
11
|
-
stimulus: ComponentControllerMap.call
|
|
12
|
-
}
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.extract_schema
|
|
16
|
-
Themes::Base::SCHEMA.transform_values do |param_schema|
|
|
17
|
-
param_schema.transform_values do |meta|
|
|
18
|
-
v = meta[:validate]
|
|
19
|
-
{ default: meta[:default], valid: v.respond_to?(:to_a) ? v.to_a : v.inspect }
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def self.extract_field_as
|
|
25
|
-
{
|
|
26
|
-
field: Form::Fields::Renderer::FIELD.keys,
|
|
27
|
-
collection_field: Form::Fields::Renderer::COLLECTION.keys,
|
|
28
|
-
choice: Form::Fields::Renderer::CHOICE.keys
|
|
29
|
-
}
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def self.extract_icons
|
|
33
|
-
heroicon_dir = Themes::Tailwind::Icons::Heroicon.send(:svg_dir)
|
|
34
|
-
custom_dir = Themes::Tailwind::Icons::Custom.send(:svg_dir)
|
|
35
|
-
|
|
36
|
-
outline = Dir[File.join(heroicon_dir, "outline", "*.svg")].map { |f| File.basename(f, ".svg") }
|
|
37
|
-
solid = Dir[File.join(heroicon_dir, "solid", "*.svg")].map { |f| "#{File.basename(f, ".svg")}/solid" }
|
|
38
|
-
customs = Dir[File.join(custom_dir, "*.svg")].map { |f| File.basename(f, ".svg") }
|
|
39
|
-
aliases = Themes::Tailwind::Icon::ALIASES.keys
|
|
40
|
-
|
|
41
|
-
(outline + solid + customs + aliases).uniq.sort
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StimulusPlumbers
|
|
4
|
-
module MCP
|
|
5
|
-
class StimulusManifestLoader
|
|
6
|
-
MANIFEST_FILENAME = "controllers.manifest.json"
|
|
7
|
-
|
|
8
|
-
def self.call
|
|
9
|
-
paths = manifest_paths
|
|
10
|
-
path = paths.find { |p| File.exist?(p) }
|
|
11
|
-
|
|
12
|
-
unless path
|
|
13
|
-
StimulusPlumbers::Logger.warn(
|
|
14
|
-
"#{MANIFEST_FILENAME} not found. Tried:\n" \
|
|
15
|
-
"#{paths.map { |p| " - #{p}" }.join("\n")}\n" \
|
|
16
|
-
"Run `node --run build:manifest` in stimulus-plumbers/ to generate it."
|
|
17
|
-
)
|
|
18
|
-
return {}
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
JSON.parse(File.read(path))
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def self.manifest_paths
|
|
25
|
-
[
|
|
26
|
-
# 1. npm/yarn/bun project — version matches project lockfile
|
|
27
|
-
File.join(Dir.pwd, "node_modules/@stimulus-plumbers/controllers/dist", MANIFEST_FILENAME),
|
|
28
|
-
# 2. importmaps users — vendored in Rails gem at release time
|
|
29
|
-
gem_vendor_path,
|
|
30
|
-
# 3. Monorepo dev fallback
|
|
31
|
-
File.expand_path(
|
|
32
|
-
File.join(__dir__, "../../../../..", "stimulus-plumbers", "dist", MANIFEST_FILENAME)
|
|
33
|
-
)
|
|
34
|
-
].compact
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def self.gem_vendor_path
|
|
38
|
-
gem_dir = Gem::Specification.find_by_name("stimulus_plumbers").gem_dir
|
|
39
|
-
File.join(gem_dir, "vendor", MANIFEST_FILENAME)
|
|
40
|
-
rescue Gem::MissingSpecError
|
|
41
|
-
nil
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
private_class_method :manifest_paths, :gem_vendor_path
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StimulusPlumbers
|
|
4
|
-
module MCP
|
|
5
|
-
class TailwindThemeLoader
|
|
6
|
-
def self.call
|
|
7
|
-
theme = Themes::TailwindTheme.new
|
|
8
|
-
|
|
9
|
-
Themes::Base::SCHEMA.each_with_object({}) do |(key, params), result|
|
|
10
|
-
# Skip keys with no _classes method — calling resolve would trigger Logger.warn
|
|
11
|
-
next unless theme.respond_to?(:"#{key}_classes", true)
|
|
12
|
-
|
|
13
|
-
result[key] = {}
|
|
14
|
-
result[key][:default] = theme.resolve(key)[:classes].to_s
|
|
15
|
-
|
|
16
|
-
params.each do |param, meta|
|
|
17
|
-
valid = meta[:validate]
|
|
18
|
-
next unless valid.respond_to?(:to_a)
|
|
19
|
-
|
|
20
|
-
valid.to_a.each do |val|
|
|
21
|
-
classes = theme.resolve(key, param => val)[:classes].to_s
|
|
22
|
-
result[key]["#{param}:#{val}"] = classes
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StimulusPlumbers
|
|
4
|
-
module MCP
|
|
5
|
-
class ThemeLoader
|
|
6
|
-
BASE_DOC = <<~MARKDOWN
|
|
7
|
-
# Custom Theme Implementation Guide
|
|
8
|
-
|
|
9
|
-
A custom theme is a Ruby class that extends `StimulusPlumbers::Themes::Base` and defines
|
|
10
|
-
`{component_key}_classes(**args)` methods for the components you want to style.
|
|
11
|
-
|
|
12
|
-
## Method Convention
|
|
13
|
-
|
|
14
|
-
- **Name:** `{component_key}_classes` — e.g. `button_classes`, `form_group_classes`
|
|
15
|
-
- **Params:** keyword arguments matching the schema params for that component
|
|
16
|
-
- **Return:** a Hash with a `:classes` key containing a space-separated CSS class string
|
|
17
|
-
|
|
18
|
-
```ruby
|
|
19
|
-
def button_classes(type: :default, variant: :default, size: :md)
|
|
20
|
-
{ classes: "..." }
|
|
21
|
-
end
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Components with no params still receive empty kwargs:
|
|
25
|
-
|
|
26
|
-
```ruby
|
|
27
|
-
def form_group_classes
|
|
28
|
-
{ classes: "..." }
|
|
29
|
-
end
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Minimal Example
|
|
33
|
-
|
|
34
|
-
```ruby
|
|
35
|
-
class MyTheme < StimulusPlumbers::Themes::Base
|
|
36
|
-
private
|
|
37
|
-
|
|
38
|
-
def button_classes(type: :default, variant: :default, size: :md)
|
|
39
|
-
base = "inline-flex items-center gap-2 rounded px-3 py-2"
|
|
40
|
-
variant_class = { primary: "bg-blue-600 text-white", destructive: "bg-red-600 text-white" }.fetch(variant, "bg-gray-100")
|
|
41
|
-
{ classes: [base, variant_class].join(" ") }
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def link_classes(type: :default, variant: :default)
|
|
45
|
-
{ classes: "underline text-blue-600 hover:text-blue-800" }
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Registration
|
|
51
|
-
|
|
52
|
-
```ruby
|
|
53
|
-
StimulusPlumbers.configure do |config|
|
|
54
|
-
config.theme.register(:my_theme, MyTheme)
|
|
55
|
-
config.theme = :my_theme
|
|
56
|
-
end
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## Component Keys
|
|
60
|
-
|
|
61
|
-
See `theme://components` for the full list of component keys that can be themed.
|
|
62
|
-
Use `theme://components/{name}` for the method signature and param details per component.
|
|
63
|
-
|
|
64
|
-
## Partial Implementation
|
|
65
|
-
|
|
66
|
-
You only need to define methods for components you want to style — unimplemented keys
|
|
67
|
-
return an empty hash, which renders the component with no CSS classes.
|
|
68
|
-
MARKDOWN
|
|
69
|
-
|
|
70
|
-
def self.call
|
|
71
|
-
{
|
|
72
|
-
base_doc: BASE_DOC,
|
|
73
|
-
components: extract_interface
|
|
74
|
-
}
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def self.extract_interface
|
|
78
|
-
Themes::Base::SCHEMA.each_with_object({}) do |(key, params), result|
|
|
79
|
-
result[key] = {
|
|
80
|
-
method: "#{key}_classes",
|
|
81
|
-
params: params.transform_values { |meta| format_param(meta) },
|
|
82
|
-
returns: "{ classes: String }"
|
|
83
|
-
}
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def self.format_param(meta)
|
|
88
|
-
valid = meta[:validate]
|
|
89
|
-
entry = { default: meta[:default] }
|
|
90
|
-
entry[:valid] = valid.to_a if valid.respond_to?(:to_a)
|
|
91
|
-
entry
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
private_class_method :extract_interface, :format_param
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StimulusPlumbers
|
|
4
|
-
module MCP
|
|
5
|
-
module Plugins
|
|
6
|
-
module Docs
|
|
7
|
-
extend Base
|
|
8
|
-
|
|
9
|
-
LOADER_KEY = :docs
|
|
10
|
-
LOADER = DocsLoader
|
|
11
|
-
|
|
12
|
-
STATIC_RESOURCES = [].freeze
|
|
13
|
-
DYNAMIC_RESOURCE_TEMPLATES = [
|
|
14
|
-
::MCP::ResourceTemplate.new(
|
|
15
|
-
uri_template: "docs://components/{name}",
|
|
16
|
-
name: "component-docs",
|
|
17
|
-
description: "Full markdown documentation and ERB examples for a component",
|
|
18
|
-
mime_type: "text/markdown"
|
|
19
|
-
),
|
|
20
|
-
::MCP::ResourceTemplate.new(
|
|
21
|
-
uri_template: "helper://components/{name}",
|
|
22
|
-
name: "component-helper-signature",
|
|
23
|
-
description: "Full sp_ helper option surface: keyword options with defaults and slot methods",
|
|
24
|
-
mime_type: "application/json"
|
|
25
|
-
)
|
|
26
|
-
].freeze
|
|
27
|
-
|
|
28
|
-
def self.read(uri, store)
|
|
29
|
-
docs = store[:docs]
|
|
30
|
-
|
|
31
|
-
case uri
|
|
32
|
-
when %r{\Adocs://components/(.+)\z}
|
|
33
|
-
doc = docs[Regexp.last_match(1).to_sym]
|
|
34
|
-
doc ? text_resource(uri, "text/markdown", doc[:content]) : missing(uri, Regexp.last_match(1))
|
|
35
|
-
when %r{\Ahelper://components/(.+)\z}
|
|
36
|
-
doc = docs[Regexp.last_match(1).to_sym]
|
|
37
|
-
doc ? json_resource(uri, doc[:signature]) : missing(uri, Regexp.last_match(1))
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def self.missing(uri, name)
|
|
42
|
-
json_resource(uri, { error: "no documentation for: #{name}" })
|
|
43
|
-
end
|
|
44
|
-
private_class_method :missing
|
|
45
|
-
|
|
46
|
-
def self.register_tools(server, store)
|
|
47
|
-
docs = store[:docs]
|
|
48
|
-
|
|
49
|
-
text_tool(
|
|
50
|
-
server,
|
|
51
|
-
name: "list_docs",
|
|
52
|
-
description: "Lists components that have markdown docs (docs://components/{name}) and " \
|
|
53
|
-
"helper signatures (helper://components/{name})"
|
|
54
|
-
) do
|
|
55
|
-
JSON.generate(docs.keys)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
text_tool(
|
|
59
|
-
server,
|
|
60
|
-
name: "get_erb_examples",
|
|
61
|
-
description: "Returns ERB usage examples for a component from the documentation",
|
|
62
|
-
input_schema: { properties: { component: { type: "string" } }, required: ["component"] }
|
|
63
|
-
) do |component:|
|
|
64
|
-
examples = docs[component.to_sym]&.dig(:examples) || []
|
|
65
|
-
examples.empty? ? not_found("no examples for: #{component}") : examples.join("\n\n")
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
text_tool(
|
|
69
|
-
server,
|
|
70
|
-
name: "get_helper_signature",
|
|
71
|
-
description: "Returns the full sp_ helper surface for a component: keyword options with " \
|
|
72
|
-
"defaults plus slot methods (e.g. icon_leading, card.with_action)",
|
|
73
|
-
input_schema: { properties: { component: { type: "string" } }, required: ["component"] }
|
|
74
|
-
) do |component:|
|
|
75
|
-
doc = docs[component.to_sym]
|
|
76
|
-
doc ? JSON.generate(doc[:signature]) : not_found("no documentation for: #{component}")
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StimulusPlumbers
|
|
4
|
-
module MCP
|
|
5
|
-
module Plugins
|
|
6
|
-
module Schema
|
|
7
|
-
extend Base
|
|
8
|
-
|
|
9
|
-
LOADER_KEY = :schema
|
|
10
|
-
LOADER = SchemaLoader
|
|
11
|
-
|
|
12
|
-
STATIC_RESOURCES = [
|
|
13
|
-
::MCP::Resource.new(
|
|
14
|
-
uri: "schema://components",
|
|
15
|
-
name: "components-index",
|
|
16
|
-
description: "Index of all stimulus-plumbers component theme keys",
|
|
17
|
-
mime_type: "application/json"
|
|
18
|
-
),
|
|
19
|
-
::MCP::Resource.new(
|
|
20
|
-
uri: "schema://icons",
|
|
21
|
-
name: "icons",
|
|
22
|
-
description: "All available icon names from the active theme registry",
|
|
23
|
-
mime_type: "application/json"
|
|
24
|
-
),
|
|
25
|
-
::MCP::Resource.new(
|
|
26
|
-
uri: "schema://stimulus",
|
|
27
|
-
name: "stimulus-wiring",
|
|
28
|
-
description: "Mapping from Rails component name to the Stimulus controller identifiers it requires",
|
|
29
|
-
mime_type: "application/json"
|
|
30
|
-
)
|
|
31
|
-
].freeze
|
|
32
|
-
|
|
33
|
-
DYNAMIC_RESOURCE_TEMPLATES = [
|
|
34
|
-
::MCP::ResourceTemplate.new(
|
|
35
|
-
uri_template: "schema://components/{name}",
|
|
36
|
-
name: "component-schema",
|
|
37
|
-
description: "Params, valid values, defaults, and required controllers for a component",
|
|
38
|
-
mime_type: "application/json"
|
|
39
|
-
)
|
|
40
|
-
].freeze
|
|
41
|
-
|
|
42
|
-
def self.read(uri, store)
|
|
43
|
-
schema = store[:schema]
|
|
44
|
-
|
|
45
|
-
case uri
|
|
46
|
-
when "schema://components"
|
|
47
|
-
json_resource(uri, schema[:components].keys)
|
|
48
|
-
when "schema://icons"
|
|
49
|
-
json_resource(uri, schema[:icons])
|
|
50
|
-
when "schema://stimulus"
|
|
51
|
-
json_resource(uri, schema[:stimulus])
|
|
52
|
-
when %r{\Aschema://components/(.+)\z}
|
|
53
|
-
key = Regexp.last_match(1).to_sym
|
|
54
|
-
json_resource(uri, component_data(schema, key) || { error: "unknown component: #{key}" })
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def self.register_tools(server, store)
|
|
59
|
-
schema = store[:schema]
|
|
60
|
-
data_by_component = component_data_map(schema)
|
|
61
|
-
|
|
62
|
-
text_tool(server, name: "list_components", description: "Lists all stimulus-plumbers component theme keys") do
|
|
63
|
-
JSON.generate(schema[:components].keys)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
text_tool(
|
|
67
|
-
server,
|
|
68
|
-
name: "get_component_schema",
|
|
69
|
-
description: "Returns themed params (e.g. type/variant/size) with valid values, defaults, and " \
|
|
70
|
-
"required Stimulus controllers. For the full helper surface (icon options, slots) " \
|
|
71
|
-
"use get_helper_signature",
|
|
72
|
-
input_schema: { properties: { component: { type: "string" } }, required: ["component"] }
|
|
73
|
-
) do |component:|
|
|
74
|
-
data = data_by_component[component.to_sym]
|
|
75
|
-
data ? JSON.generate(data) : not_found("unknown component: #{component}")
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
text_tool(
|
|
79
|
-
server,
|
|
80
|
-
name: "get_field_types",
|
|
81
|
-
description: "Returns valid as: values for a form builder method",
|
|
82
|
-
input_schema: {
|
|
83
|
-
properties: { builder_method: { type: "string", enum: %w[field collection_field choice] } },
|
|
84
|
-
required: ["builder_method"]
|
|
85
|
-
}
|
|
86
|
-
) do |builder_method:|
|
|
87
|
-
values = schema[:field_as][builder_method.to_sym]
|
|
88
|
-
values ? JSON.generate(values) : not_found("unknown builder_method: #{builder_method}")
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Resolve component_data at module scope; define_tool blocks run in another context.
|
|
93
|
-
def self.component_data_map(schema)
|
|
94
|
-
keys = (schema[:components].keys + schema[:stimulus].keys).uniq
|
|
95
|
-
keys.to_h { |key| [key, component_data(schema, key)] }
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def self.component_data(schema, key)
|
|
99
|
-
params = schema[:components][key]
|
|
100
|
-
wiring = schema[:stimulus]
|
|
101
|
-
return nil unless params || wiring.key?(key)
|
|
102
|
-
|
|
103
|
-
(params || {}).merge(controllers: wiring[key] || [])
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
private_class_method :component_data_map, :component_data
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StimulusPlumbers
|
|
4
|
-
module MCP
|
|
5
|
-
module Plugins
|
|
6
|
-
module Stimulus
|
|
7
|
-
extend Base
|
|
8
|
-
|
|
9
|
-
LOADER_KEY = :stimulus
|
|
10
|
-
LOADER = StimulusManifestLoader
|
|
11
|
-
|
|
12
|
-
STATIC_RESOURCES = [
|
|
13
|
-
::MCP::Resource.new(
|
|
14
|
-
uri: "stimulus://controllers",
|
|
15
|
-
name: "stimulus-controllers-index",
|
|
16
|
-
description: "Index of all Stimulus controller identifiers in @stimulus-plumbers/controllers",
|
|
17
|
-
mime_type: "application/json"
|
|
18
|
-
)
|
|
19
|
-
].freeze
|
|
20
|
-
|
|
21
|
-
DYNAMIC_RESOURCE_TEMPLATES = [
|
|
22
|
-
::MCP::ResourceTemplate.new(
|
|
23
|
-
uri_template: "stimulus://controllers/{identifier}",
|
|
24
|
-
name: "stimulus-controller-schema",
|
|
25
|
-
description: "Targets, values, outlets, and classes for a Stimulus controller",
|
|
26
|
-
mime_type: "application/json"
|
|
27
|
-
)
|
|
28
|
-
].freeze
|
|
29
|
-
|
|
30
|
-
def self.read(uri, store)
|
|
31
|
-
controllers = store[:stimulus]
|
|
32
|
-
|
|
33
|
-
case uri
|
|
34
|
-
when "stimulus://controllers"
|
|
35
|
-
json_resource(uri, controllers.keys)
|
|
36
|
-
when %r{\Astimulus://controllers/(.+)\z}
|
|
37
|
-
identifier = Regexp.last_match(1)
|
|
38
|
-
json_resource(uri, controllers[identifier] || { error: "unknown controller: #{identifier}" })
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def self.register_tools(server, store)
|
|
43
|
-
controllers = store[:stimulus]
|
|
44
|
-
|
|
45
|
-
text_tool(
|
|
46
|
-
server,
|
|
47
|
-
name: "list_controllers",
|
|
48
|
-
description: "Lists all Stimulus controller identifiers provided by @stimulus-plumbers/controllers"
|
|
49
|
-
) do
|
|
50
|
-
JSON.generate(controllers.keys)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
text_tool(
|
|
54
|
-
server,
|
|
55
|
-
name: "get_controller_schema",
|
|
56
|
-
description: "Returns targets, values (with types and defaults), outlets, and classes for a Stimulus controller",
|
|
57
|
-
input_schema: { properties: { controller: { type: "string" } }, required: ["controller"] }
|
|
58
|
-
) do |controller:|
|
|
59
|
-
data = controllers[controller]
|
|
60
|
-
data ? JSON.generate(data) : not_found("unknown controller: #{controller}")
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|