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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -24
  3. data/lib/stimulus_plumbers/mcp/loaders/aria_loader.rb +31 -0
  4. data/lib/stimulus_plumbers/mcp/loaders/component_docs_loader.rb +38 -0
  5. data/lib/stimulus_plumbers/mcp/loaders/component_requirements.rb +41 -0
  6. data/lib/stimulus_plumbers/mcp/loaders/component_schema_loader.rb +49 -0
  7. data/lib/stimulus_plumbers/mcp/loaders/component_theme_loader.rb +41 -0
  8. data/lib/stimulus_plumbers/mcp/loaders/controller_docs_loader.rb +31 -0
  9. data/lib/stimulus_plumbers/mcp/loaders/controller_schema_loader.rb +49 -0
  10. data/lib/stimulus_plumbers/mcp/loaders/guide.md +48 -0
  11. data/lib/stimulus_plumbers/mcp/loaders/guide_loader.rb +40 -4
  12. data/lib/stimulus_plumbers/mcp/loaders/icons_loader.rb +34 -0
  13. data/lib/stimulus_plumbers/mcp/loaders/support/docs_table_parser.rb +112 -0
  14. data/lib/stimulus_plumbers/mcp/loaders/support/gem_vendor_path.rb +18 -0
  15. data/lib/stimulus_plumbers/mcp/loaders/tailwind_loader.rb +35 -0
  16. data/lib/stimulus_plumbers/mcp/loaders/versions_loader.rb +110 -0
  17. data/lib/stimulus_plumbers/mcp/plugins/aria.rb +34 -0
  18. data/lib/stimulus_plumbers/mcp/plugins/base.rb +40 -31
  19. data/lib/stimulus_plumbers/mcp/plugins/component_docs.rb +96 -0
  20. data/lib/stimulus_plumbers/mcp/plugins/component_schema.rb +133 -0
  21. data/lib/stimulus_plumbers/mcp/plugins/component_theme.rb +90 -0
  22. data/lib/stimulus_plumbers/mcp/plugins/controller_docs.rb +91 -0
  23. data/lib/stimulus_plumbers/mcp/plugins/controller_schema.rb +81 -0
  24. data/lib/stimulus_plumbers/mcp/plugins/guide.rb +70 -16
  25. data/lib/stimulus_plumbers/mcp/plugins/icons.rb +42 -0
  26. data/lib/stimulus_plumbers/mcp/plugins/tailwind.rb +69 -45
  27. data/lib/stimulus_plumbers/mcp/plugins/versions.rb +44 -0
  28. data/lib/stimulus_plumbers/mcp/server.rb +26 -9
  29. data/lib/stimulus_plumbers/mcp/version.rb +1 -1
  30. data/lib/stimulus_plumbers_mcp.rb +31 -11
  31. metadata +22 -12
  32. data/lib/stimulus_plumbers/mcp/loaders/component_controller_map.rb +0 -38
  33. data/lib/stimulus_plumbers/mcp/loaders/docs_loader.rb +0 -129
  34. data/lib/stimulus_plumbers/mcp/loaders/guide/overview.md +0 -48
  35. data/lib/stimulus_plumbers/mcp/loaders/schema_loader.rb +0 -45
  36. data/lib/stimulus_plumbers/mcp/loaders/stimulus_manifest.rb +0 -47
  37. data/lib/stimulus_plumbers/mcp/loaders/tailwind_theme_loader.rb +0 -29
  38. data/lib/stimulus_plumbers/mcp/loaders/theme_loader.rb +0 -97
  39. data/lib/stimulus_plumbers/mcp/plugins/docs.rb +0 -82
  40. data/lib/stimulus_plumbers/mcp/plugins/schema.rb +0 -110
  41. data/lib/stimulus_plumbers/mcp/plugins/stimulus.rb +0 -66
  42. 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