stimulus_plumbers_mcp 0.4.4
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 +7 -0
- data/lib/stimulus_plumbers/mcp/loaders/component_controller_map.rb +38 -0
- data/lib/stimulus_plumbers/mcp/loaders/docs_loader.rb +122 -0
- data/lib/stimulus_plumbers/mcp/loaders/guide_loader.rb +15 -0
- data/lib/stimulus_plumbers/mcp/loaders/schema_loader.rb +45 -0
- data/lib/stimulus_plumbers/mcp/loaders/stimulus_manifest.rb +23 -0
- data/lib/stimulus_plumbers/mcp/loaders/tailwind_theme_loader.rb +29 -0
- data/lib/stimulus_plumbers/mcp/loaders/theme_loader.rb +97 -0
- data/lib/stimulus_plumbers/mcp/plugins/base.rb +49 -0
- data/lib/stimulus_plumbers/mcp/plugins/docs.rb +82 -0
- data/lib/stimulus_plumbers/mcp/plugins/guide.rb +31 -0
- data/lib/stimulus_plumbers/mcp/plugins/schema.rb +110 -0
- data/lib/stimulus_plumbers/mcp/plugins/stimulus.rb +66 -0
- data/lib/stimulus_plumbers/mcp/plugins/tailwind.rb +58 -0
- data/lib/stimulus_plumbers/mcp/plugins/theme.rb +66 -0
- data/lib/stimulus_plumbers/mcp/server.rb +66 -0
- data/lib/stimulus_plumbers/mcp/version.rb +7 -0
- data/lib/stimulus_plumbers_mcp.rb +26 -0
- metadata +114 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 00ce340e2b2d68fcfef0406efd76e88e91c5214c653bc44cbed78d6ebf23bd4e
|
|
4
|
+
data.tar.gz: c4ec9ad1abdabfb7ab464b685e604d78feec3a67b10944339885adbd5f322668
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 752269bd37dc2716c2fdabf2aab3bb6cecb9bd6aaae82ff245c95414ef98c9a670ac1da896a32dafa00b9d47b8231214b5f150f3ca1efd7705faafe978eabccb
|
|
7
|
+
data.tar.gz: 8540984025d8870982f1d75d8a33a51dfcb3096084eb8c8d0ef0dc5cc39f62b02310f787568a0d34a8cb49ac055273b6f2fa7254b398a67703879d750dceb9f5
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
class ComponentControllerMap
|
|
6
|
+
def self.call
|
|
7
|
+
Components.constants
|
|
8
|
+
.map { |c| Components.const_get(c) }
|
|
9
|
+
.grep(Class)
|
|
10
|
+
.to_h { |klass| [component_key(klass), controllers_for(klass).uniq] }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Controllers from a component class plus its nested sub-components (e.g. Combobox::Date),
|
|
14
|
+
# keyed to the top-level component. Skips references to sibling components.
|
|
15
|
+
def self.controllers_for(mod)
|
|
16
|
+
mod.constants(false).flat_map do |const|
|
|
17
|
+
value = mod.const_get(const)
|
|
18
|
+
if const.to_s.end_with?("CONTROLLER")
|
|
19
|
+
Array(value).grep(String).flat_map(&:split)
|
|
20
|
+
elsif nested?(mod, value)
|
|
21
|
+
controllers_for(value)
|
|
22
|
+
else
|
|
23
|
+
[]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.nested?(mod, value)
|
|
29
|
+
value.is_a?(Module) && !value.name.nil? && value.name.start_with?("#{mod.name}::")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.component_key(klass)
|
|
33
|
+
klass.name.demodulize.underscore.to_sym
|
|
34
|
+
end
|
|
35
|
+
private_class_method :controllers_for, :nested?, :component_key
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
class DocsLoader
|
|
6
|
+
DOCS_DIR = File.expand_path(
|
|
7
|
+
"../../../../../stimulus-plumbers-rails/docs/component",
|
|
8
|
+
__dir__
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
def self.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: extract_signature(content)
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.extract_erb_examples(content)
|
|
24
|
+
content.scan(%r{```erb\n(.*?)```}m).map(&:first).map(&:strip)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Parse `| Option |` / `| Slot method |` doc tables into the helper surface.
|
|
28
|
+
# Options are grouped under the heading (sub-helper signature) above them.
|
|
29
|
+
def self.extract_signature(content)
|
|
30
|
+
tables = tables_with_headings(content)
|
|
31
|
+
{ helpers: option_helpers(tables), slots: slot_methods(tables) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.option_helpers(tables)
|
|
35
|
+
tables.select { |t| t[:header].first == "Option" }
|
|
36
|
+
.filter_map do |t|
|
|
37
|
+
options = t[:rows].map { |r| option_row(r) }
|
|
38
|
+
{ signature: t[:heading], options: options } unless options.empty?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.slot_methods(tables)
|
|
43
|
+
tables.select { |t| t[:header].first == "Slot method" }
|
|
44
|
+
.flat_map { |t| t[:rows].map { |r| slot_row(r) } }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.option_row(cells)
|
|
48
|
+
{ option: clean(cells[0]), default: clean(cells[1]), description: cells[2].to_s }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.slot_row(cells)
|
|
52
|
+
slot = clean(cells[0])
|
|
53
|
+
description = cells[1].to_s
|
|
54
|
+
{ slot: slot, description: description, block: block_required?(slot, description) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.block_required?(slot, description)
|
|
58
|
+
slot.include?("{") || description.match?(%r{block required}i)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Tag each table with the heading above it; skip fenced code (so ```ruby
|
|
62
|
+
# comments aren't read as headings).
|
|
63
|
+
def self.tables_with_headings(content)
|
|
64
|
+
state = { heading: nil, fenced: false, buffer: [], tables: [] }
|
|
65
|
+
content.each_line { |line| scan_line(line, state) }
|
|
66
|
+
flush_table(state)
|
|
67
|
+
state[:tables]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.scan_line(line, state)
|
|
71
|
+
if line.start_with?("```")
|
|
72
|
+
flush_table(state)
|
|
73
|
+
state[:fenced] = !state[:fenced]
|
|
74
|
+
elsif state[:fenced]
|
|
75
|
+
nil
|
|
76
|
+
elsif (heading = line[%r{\A#+\s+(.+)}, 1])
|
|
77
|
+
flush_table(state)
|
|
78
|
+
state[:heading] = clean(heading)
|
|
79
|
+
elsif line.lstrip.start_with?("|")
|
|
80
|
+
state[:buffer] << line
|
|
81
|
+
else
|
|
82
|
+
flush_table(state)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.flush_table(state)
|
|
87
|
+
return if state[:buffer].empty?
|
|
88
|
+
|
|
89
|
+
state[:tables] << build_table(state[:buffer]).merge(heading: state[:heading])
|
|
90
|
+
state[:buffer] = []
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.build_table(lines)
|
|
94
|
+
rows = lines.map { |l| split_row(l) }.reject { |cells| cells.all? { |c| c.match?(%r{\A:?-+:?\z}) } }
|
|
95
|
+
{ header: rows.first, rows: rows.drop(1) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Split a markdown table row, honouring escaped pipes (`\|`) inside cells.
|
|
99
|
+
def self.split_row(line)
|
|
100
|
+
line.strip.delete_prefix("|").delete_suffix("|")
|
|
101
|
+
.split(%r{(?<!\\)\|})
|
|
102
|
+
.map { |c| c.gsub('\|', "|").strip }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.clean(cell)
|
|
106
|
+
cell.to_s.gsub(%r{[`*]}, "").sub(%r{:\z}, "").strip
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private_class_method :option_helpers,
|
|
110
|
+
:slot_methods,
|
|
111
|
+
:option_row,
|
|
112
|
+
:slot_row,
|
|
113
|
+
:block_required?,
|
|
114
|
+
:tables_with_headings,
|
|
115
|
+
:scan_line,
|
|
116
|
+
:flush_table,
|
|
117
|
+
:build_table,
|
|
118
|
+
:split_row,
|
|
119
|
+
:clean
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
class GuideLoader
|
|
6
|
+
OVERVIEW_PATH = File.expand_path("guide/overview.md", __dir__).freeze
|
|
7
|
+
|
|
8
|
+
def self.call
|
|
9
|
+
return "" unless File.exist?(OVERVIEW_PATH)
|
|
10
|
+
|
|
11
|
+
File.read(OVERVIEW_PATH)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
class StimulusManifest
|
|
6
|
+
MANIFEST_PATH = File.expand_path(
|
|
7
|
+
File.join(__dir__, "../../../../..", "stimulus-plumbers", "dist", "controllers.manifest.json")
|
|
8
|
+
).freeze
|
|
9
|
+
|
|
10
|
+
def self.call
|
|
11
|
+
unless File.exist?(MANIFEST_PATH)
|
|
12
|
+
StimulusPlumbers::Logger.warn(
|
|
13
|
+
"controllers.manifest.json not found at #{MANIFEST_PATH}. " \
|
|
14
|
+
"Run `node --run build:manifest` in stimulus-plumbers/ to generate it."
|
|
15
|
+
)
|
|
16
|
+
return {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
JSON.parse(File.read(MANIFEST_PATH))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
module Plugins
|
|
6
|
+
# Shared contract + content helpers for plugins, which `extend` this.
|
|
7
|
+
#
|
|
8
|
+
# Each plugin defines: LOADER_KEY, LOADER, STATIC_RESOURCES,
|
|
9
|
+
# DYNAMIC_RESOURCE_TEMPLATES, and read(uri, store). Tools are optional —
|
|
10
|
+
# plugins with tools override register_tools, the rest inherit the no-op.
|
|
11
|
+
module Base
|
|
12
|
+
# Returned by a tool block to signal "not found" — rendered as an MCP
|
|
13
|
+
# error response with a structured { error: } payload (see text_tool).
|
|
14
|
+
NotFound = Struct.new(:message)
|
|
15
|
+
|
|
16
|
+
def register_tools(_server, _store); end
|
|
17
|
+
|
|
18
|
+
def not_found(message)
|
|
19
|
+
NotFound.new(message)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# resources/read content for a JSON payload.
|
|
23
|
+
def json_resource(uri, data)
|
|
24
|
+
[{ uri: uri, mimeType: "application/json", text: JSON.generate(data) }]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# resources/read content for raw text (e.g. markdown).
|
|
28
|
+
def text_resource(uri, mime_type, text)
|
|
29
|
+
[{ uri: uri, mimeType: mime_type, text: text }]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Define a tool whose block returns the response text, or `not_found(msg)`
|
|
33
|
+
# for a uniform error (isError + { error: } JSON). Declaring `**args` makes
|
|
34
|
+
# the MCP gem inject :server_context, which tool blocks don't want — drop it.
|
|
35
|
+
def text_tool(server, name:, description:, input_schema: nil, &block)
|
|
36
|
+
server.define_tool(name: name, description: description, input_schema: input_schema) do |**args|
|
|
37
|
+
args.delete(:server_context)
|
|
38
|
+
result = block.call(**args)
|
|
39
|
+
if result.is_a?(NotFound)
|
|
40
|
+
::MCP::Tool::Response.new([{ type: "text", text: JSON.generate(error: result.message) }], error: true)
|
|
41
|
+
else
|
|
42
|
+
::MCP::Tool::Response.new([{ type: "text", text: result }])
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
module Plugins
|
|
6
|
+
module Guide
|
|
7
|
+
extend Base
|
|
8
|
+
|
|
9
|
+
LOADER_KEY = :guide
|
|
10
|
+
LOADER = GuideLoader
|
|
11
|
+
|
|
12
|
+
STATIC_RESOURCES = [
|
|
13
|
+
::MCP::Resource.new(
|
|
14
|
+
uri: "guide://overview",
|
|
15
|
+
name: "overview",
|
|
16
|
+
description: "Start here — how to build views and forms with stimulus-plumbers, with pointers to every tool/resource",
|
|
17
|
+
mime_type: "text/markdown"
|
|
18
|
+
)
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
DYNAMIC_RESOURCE_TEMPLATES = [].freeze
|
|
22
|
+
|
|
23
|
+
def self.read(uri, store)
|
|
24
|
+
return unless uri == "guide://overview"
|
|
25
|
+
|
|
26
|
+
text_resource(uri, "text/markdown", store[:guide])
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
|
@@ -0,0 +1,66 @@
|
|
|
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 = StimulusManifest
|
|
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
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
module Plugins
|
|
6
|
+
module Tailwind
|
|
7
|
+
extend Base
|
|
8
|
+
|
|
9
|
+
LOADER_KEY = :tailwind
|
|
10
|
+
LOADER = TailwindThemeLoader
|
|
11
|
+
|
|
12
|
+
STATIC_RESOURCES = [
|
|
13
|
+
::MCP::Resource.new(
|
|
14
|
+
uri: "tailwind://components",
|
|
15
|
+
name: "tailwind-components-index",
|
|
16
|
+
description: "Index of component keys implemented by the stimulus-plumbers Tailwind theme",
|
|
17
|
+
mime_type: "application/json"
|
|
18
|
+
)
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
DYNAMIC_RESOURCE_TEMPLATES = [
|
|
22
|
+
::MCP::ResourceTemplate.new(
|
|
23
|
+
uri_template: "tailwind://components/{name}",
|
|
24
|
+
name: "tailwind-component-classes",
|
|
25
|
+
description: "Tailwind CSS utility classes emitted per variant for a component",
|
|
26
|
+
mime_type: "application/json"
|
|
27
|
+
)
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
def self.read(uri, store)
|
|
31
|
+
tailwind = store[:tailwind]
|
|
32
|
+
|
|
33
|
+
case uri
|
|
34
|
+
when "tailwind://components"
|
|
35
|
+
json_resource(uri, tailwind.keys)
|
|
36
|
+
when %r{\Atailwind://components/(.+)\z}
|
|
37
|
+
key = Regexp.last_match(1).to_sym
|
|
38
|
+
json_resource(uri, tailwind[key] || { error: "unknown component: #{key}" })
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.register_tools(server, store)
|
|
43
|
+
tailwind = store[:tailwind]
|
|
44
|
+
|
|
45
|
+
text_tool(
|
|
46
|
+
server,
|
|
47
|
+
name: "get_tailwind_classes",
|
|
48
|
+
description: "Returns Tailwind CSS utility classes emitted per variant for a component",
|
|
49
|
+
input_schema: { properties: { component: { type: "string" } }, required: ["component"] }
|
|
50
|
+
) do |component:|
|
|
51
|
+
data = tailwind[component.to_sym]
|
|
52
|
+
data ? JSON.generate(data) : not_found("unknown component: #{component}")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
module Plugins
|
|
6
|
+
module Theme
|
|
7
|
+
extend Base
|
|
8
|
+
|
|
9
|
+
LOADER_KEY = :theme
|
|
10
|
+
LOADER = ThemeLoader
|
|
11
|
+
|
|
12
|
+
STATIC_RESOURCES = [
|
|
13
|
+
::MCP::Resource.new(
|
|
14
|
+
uri: "theme://base",
|
|
15
|
+
name: "theme-base",
|
|
16
|
+
description: "Guide for implementing a custom stimulus-plumbers theme: method convention, return format",
|
|
17
|
+
mime_type: "text/markdown"
|
|
18
|
+
),
|
|
19
|
+
::MCP::Resource.new(
|
|
20
|
+
uri: "theme://components",
|
|
21
|
+
name: "theme-components-index",
|
|
22
|
+
description: "Index of all component keys that can be implemented in a custom theme",
|
|
23
|
+
mime_type: "application/json"
|
|
24
|
+
)
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
DYNAMIC_RESOURCE_TEMPLATES = [
|
|
28
|
+
::MCP::ResourceTemplate.new(
|
|
29
|
+
uri_template: "theme://components/{name}",
|
|
30
|
+
name: "theme-component-interface",
|
|
31
|
+
description: "Method name, param signature, and return contract for implementing a component in a custom theme",
|
|
32
|
+
mime_type: "application/json"
|
|
33
|
+
)
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
def self.read(uri, store)
|
|
37
|
+
theme = store[:theme]
|
|
38
|
+
|
|
39
|
+
case uri
|
|
40
|
+
when "theme://base"
|
|
41
|
+
text_resource(uri, "text/markdown", theme[:base_doc])
|
|
42
|
+
when "theme://components"
|
|
43
|
+
json_resource(uri, theme[:components].keys)
|
|
44
|
+
when %r{\Atheme://components/(.+)\z}
|
|
45
|
+
key = Regexp.last_match(1).to_sym
|
|
46
|
+
json_resource(uri, theme[:components][key] || { error: "unknown component: #{key}" })
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.register_tools(server, store)
|
|
51
|
+
theme = store[:theme]
|
|
52
|
+
|
|
53
|
+
text_tool(
|
|
54
|
+
server,
|
|
55
|
+
name: "get_theme_interface",
|
|
56
|
+
description: "Returns the method name, param signature, and return contract for a custom theme component",
|
|
57
|
+
input_schema: { properties: { component: { type: "string" } }, required: ["component"] }
|
|
58
|
+
) do |component:|
|
|
59
|
+
data = theme[:components][component.to_sym]
|
|
60
|
+
data ? JSON.generate(data) : not_found("unknown component: #{component}")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
module Server
|
|
6
|
+
PLUGINS = [
|
|
7
|
+
Plugins::Guide,
|
|
8
|
+
Plugins::Schema,
|
|
9
|
+
Plugins::Docs,
|
|
10
|
+
Plugins::Stimulus,
|
|
11
|
+
Plugins::Theme,
|
|
12
|
+
Plugins::Tailwind
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
INSTRUCTIONS = "Use these resources and tools for accurate API references when " \
|
|
16
|
+
"generating Rails/ERB view code with the stimulus-plumbers UI library. " \
|
|
17
|
+
"Read guide://overview first for a map of the form/view/stimulus API."
|
|
18
|
+
|
|
19
|
+
def self.build
|
|
20
|
+
store = PLUGINS.to_h { |plugin| [plugin::LOADER_KEY, plugin::LOADER.call] }
|
|
21
|
+
report_sources(store)
|
|
22
|
+
|
|
23
|
+
server = new_server
|
|
24
|
+
server.resources_read_handler do |params|
|
|
25
|
+
PLUGINS.lazy.filter_map { |plugin| plugin.read(params[:uri], store) }.first || []
|
|
26
|
+
end
|
|
27
|
+
PLUGINS.each { |plugin| plugin.register_tools(server, store) }
|
|
28
|
+
server
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.new_server
|
|
32
|
+
::MCP::Server.new(
|
|
33
|
+
name: "stimulus-plumbers",
|
|
34
|
+
version: StimulusPlumbers::MCP::VERSION,
|
|
35
|
+
instructions: INSTRUCTIONS,
|
|
36
|
+
resources: PLUGINS.flat_map { |p| p::STATIC_RESOURCES },
|
|
37
|
+
resource_templates: PLUGINS.flat_map { |p| p::DYNAMIC_RESOURCE_TEMPLATES }
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Loaders fail soft (sibling paths may be absent), so report what resolved
|
|
42
|
+
# and warn loudly on any empty source instead of starting silently wrong.
|
|
43
|
+
def self.report_sources(store)
|
|
44
|
+
summary = store.map { |key, value| "#{key}=#{source_size(value)}" }.join(" ")
|
|
45
|
+
StimulusPlumbers::Logger.info("sources: #{summary}")
|
|
46
|
+
store.each_key do |key|
|
|
47
|
+
StimulusPlumbers::Logger.warn("source '#{key}' is empty") if empty_source?(store[key])
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.source_size(value)
|
|
52
|
+
case value
|
|
53
|
+
when String then value.empty? ? 0 : "ok"
|
|
54
|
+
when Hash then value.key?(:components) ? value[:components].size : value.size
|
|
55
|
+
when Enumerable then value.size
|
|
56
|
+
else value.nil? ? 0 : 1
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.empty_source?(value)
|
|
61
|
+
content = value.is_a?(Hash) && value.key?(:components) ? value[:components] : value
|
|
62
|
+
content.respond_to?(:empty?) && content.empty?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "action_view/version"
|
|
5
|
+
require "uri" if ActionView.version < Gem::Version.new("7.3")
|
|
6
|
+
require "mcp"
|
|
7
|
+
require "action_view"
|
|
8
|
+
require "stimulus_plumbers"
|
|
9
|
+
require "stimulus_plumbers_tailwind"
|
|
10
|
+
|
|
11
|
+
require_relative "stimulus_plumbers/mcp/version"
|
|
12
|
+
require_relative "stimulus_plumbers/mcp/loaders/guide_loader"
|
|
13
|
+
require_relative "stimulus_plumbers/mcp/loaders/schema_loader"
|
|
14
|
+
require_relative "stimulus_plumbers/mcp/loaders/docs_loader"
|
|
15
|
+
require_relative "stimulus_plumbers/mcp/loaders/stimulus_manifest"
|
|
16
|
+
require_relative "stimulus_plumbers/mcp/loaders/component_controller_map"
|
|
17
|
+
require_relative "stimulus_plumbers/mcp/loaders/theme_loader"
|
|
18
|
+
require_relative "stimulus_plumbers/mcp/loaders/tailwind_theme_loader"
|
|
19
|
+
require_relative "stimulus_plumbers/mcp/plugins/base"
|
|
20
|
+
require_relative "stimulus_plumbers/mcp/plugins/guide"
|
|
21
|
+
require_relative "stimulus_plumbers/mcp/plugins/schema"
|
|
22
|
+
require_relative "stimulus_plumbers/mcp/plugins/docs"
|
|
23
|
+
require_relative "stimulus_plumbers/mcp/plugins/stimulus"
|
|
24
|
+
require_relative "stimulus_plumbers/mcp/plugins/theme"
|
|
25
|
+
require_relative "stimulus_plumbers/mcp/plugins/tailwind"
|
|
26
|
+
require_relative "stimulus_plumbers/mcp/server"
|
metadata
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: stimulus_plumbers_mcp
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.4.4
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ryan Chang
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: actionview
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: mcp
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.8'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.8'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: stimulus_plumbers
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: stimulus_plumbers_tailwind
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
description: A local MCP server that exposes the stimulus-plumbers API schema and
|
|
69
|
+
documentation to LLM-powered IDEs
|
|
70
|
+
email:
|
|
71
|
+
- ryancyq@gmail.com
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- lib/stimulus_plumbers/mcp/loaders/component_controller_map.rb
|
|
77
|
+
- lib/stimulus_plumbers/mcp/loaders/docs_loader.rb
|
|
78
|
+
- lib/stimulus_plumbers/mcp/loaders/guide_loader.rb
|
|
79
|
+
- lib/stimulus_plumbers/mcp/loaders/schema_loader.rb
|
|
80
|
+
- lib/stimulus_plumbers/mcp/loaders/stimulus_manifest.rb
|
|
81
|
+
- lib/stimulus_plumbers/mcp/loaders/tailwind_theme_loader.rb
|
|
82
|
+
- lib/stimulus_plumbers/mcp/loaders/theme_loader.rb
|
|
83
|
+
- lib/stimulus_plumbers/mcp/plugins/base.rb
|
|
84
|
+
- lib/stimulus_plumbers/mcp/plugins/docs.rb
|
|
85
|
+
- lib/stimulus_plumbers/mcp/plugins/guide.rb
|
|
86
|
+
- lib/stimulus_plumbers/mcp/plugins/schema.rb
|
|
87
|
+
- lib/stimulus_plumbers/mcp/plugins/stimulus.rb
|
|
88
|
+
- lib/stimulus_plumbers/mcp/plugins/tailwind.rb
|
|
89
|
+
- lib/stimulus_plumbers/mcp/plugins/theme.rb
|
|
90
|
+
- lib/stimulus_plumbers/mcp/server.rb
|
|
91
|
+
- lib/stimulus_plumbers/mcp/version.rb
|
|
92
|
+
- lib/stimulus_plumbers_mcp.rb
|
|
93
|
+
homepage: https://github.com/ryancyq/stimulus-plumbers
|
|
94
|
+
licenses:
|
|
95
|
+
- MIT
|
|
96
|
+
metadata: {}
|
|
97
|
+
rdoc_options: []
|
|
98
|
+
require_paths:
|
|
99
|
+
- lib
|
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: '3.0'
|
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
requirements: []
|
|
111
|
+
rubygems_version: 3.6.9
|
|
112
|
+
specification_version: 4
|
|
113
|
+
summary: MCP server for the stimulus-plumbers UI library
|
|
114
|
+
test_files: []
|