coatepec 0.4.1 → 0.5.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0db2635670e0fdfe695b2ba6b7fa8ffb6d1aa0400adf39eaee7bfc567e1c810
4
- data.tar.gz: a236a2a6a096632f35d1442fad90656f3606389593b05baec61ad0c9dcbdf7ea
3
+ metadata.gz: 161aafede82c7279701f75a51cc7976a6ef7bbbadff7bc22a7bc04dc050e1780
4
+ data.tar.gz: 2b867df0d495bb573e1e00ad9a94f9aca800fa7045bbf6965fb0825acfc2683f
5
5
  SHA512:
6
- metadata.gz: 7cff582a4b6aa94543265820d79f28769e1a5f5cca3b121f68ce8493d6a08c6f472628e6a81ff15f69f75d9b1f547233010957264fadd86cb554c86d54796b74
7
- data.tar.gz: 79e09341a8ae0780da47ec7ab12953fea0cb8eb912f18582e6c21c27f99161b1d1cd1f6881c4f8029c4611deab6df3b8193bd56a902bcc651a50e408b06f421e
6
+ metadata.gz: d93777d12dac89c55450377259fc7d206bd6ef077766395f1397045372a58a7203bf986bbb9a525fc89b8510159a7fd9d0c3a057c321219edd95d6f23caa7acf
7
+ data.tar.gz: 3e580288a7c4ef9b0e707909967c28293ce8bc8b749e8c11d03903faef69ef80587c4a8307e13e769633d521b6245a05f806859d124a40346a9a0120b56e3e9b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1
4
+
5
+ - Fix `rails_model` crashing outright for a model with a `has_one`/
6
+ `has_many :through` association that goes through a polymorphic
7
+ `belongs_to` (e.g. `has_one :x, through: :notable, source: :y` where
8
+ `belongs_to :notable, polymorphic: true`). `foreign_key` on that
9
+ reflection needs a single fixed class to resolve, which a polymorphic
10
+ association can't provide, and that was previously an unrescued
11
+ `ArgumentError` that took down the whole response. That specific case now
12
+ reports `foreign_key: nil`/`class_name: nil` for the affected association,
13
+ the same way an already-handled plain polymorphic `belongs_to` does.
14
+ - Add a general safety net around each association's metadata: if a single
15
+ association still fails for some other, not-yet-anticipated
16
+ `ActiveRecord` reflection quirk, only that association's entry degrades
17
+ (gaining an `error` field describing what went wrong) instead of the
18
+ entire `rails_model` call crashing for the whole model.
19
+
20
+ ## 0.5.0
21
+
22
+ - **Breaking:** `mcp` is no longer a runtime dependency of the `coatepec`
23
+ gem -- it's a development dependency, since Coatepec is meant to be
24
+ installed once outside any Rails app's own bundle and pointed at the app
25
+ via `--root`, not added to the app's `Gemfile` (`Worker::Client` resolves
26
+ the worker's `RUBYLIB` from Coatepec's own installation regardless).
27
+ Anyone whose MCP config currently launches Coatepec with `bundle exec
28
+ coatepec` from inside a target app's bundle will hit a `LoadError` on
29
+ upgrade once that bundle no longer pulls in `mcp` transitively.
30
+
31
+ **Migration:** `gem install mcp` alongside `coatepec`, and change your MCP
32
+ client config to invoke `coatepec` directly rather than `bundle exec
33
+ coatepec` (drop the `coatepec` line from the target app's `Gemfile` too,
34
+ if present -- see the README Quickstart for the corrected install story).
35
+
3
36
  ## 0.4.1
4
37
 
5
38
  - Fix `exe/coatepec-worker` booting against a target app whose locally
data/README.md CHANGED
@@ -6,26 +6,31 @@ console.
6
6
 
7
7
  ## Quickstart
8
8
 
9
- ```ruby
10
- # Gemfile
11
- group :development, :test do
12
- gem "coatepec", require: false
13
- end
14
- ```
9
+ Coatepec is installed once, outside of any Rails app's own bundle, and
10
+ points at the app via `--root`:
15
11
 
16
12
  ```bash
17
- bundle install
18
- bundle exec coatepec --version
13
+ gem install coatepec mcp
14
+ coatepec --version
19
15
  ```
20
16
 
21
- Configure your MCP client to run `bundle exec coatepec --root /absolute/path/to/app`
22
- from the Rails application's own bundle.
17
+ Do not add `coatepec` to the target app's `Gemfile`. `Worker::Client#spawn_worker`
18
+ resolves both the worker executable and its `RUBYLIB` from Coatepec's own
19
+ installation, not from the app's bundle -- so the worker gets Coatepec's
20
+ `lib` regardless of what the app's Gemfile says. Adding it there is not just
21
+ redundant, it's a version-skew hazard: Bundler would activate whatever
22
+ version is in the app's lockfile while Coatepec itself keeps running the
23
+ version on `RUBYLIB`, and a drift between the two surfaces as a confusing
24
+ "already activated" failure. The only thing Coatepec needs from the target
25
+ app is `railties`, which is definitionally present in any Rails app you'd
26
+ point it at. Host app footprint is zero: no Gemfile line, no lockfile
27
+ change, no dependency resolution.
23
28
 
24
29
  With the [Claude Code CLI](https://docs.claude.com/en/docs/claude-code),
25
30
  from the Rails app's own root:
26
31
 
27
32
  ```bash
28
- claude mcp add coatepec --scope project -- bundle exec coatepec --root .
33
+ claude mcp add coatepec --scope project -- coatepec --root .
29
34
  ```
30
35
 
31
36
  That writes a project-scoped `.mcp.json` you can commit so the whole team
@@ -36,8 +41,8 @@ same thing looks like:
36
41
  {
37
42
  "mcpServers": {
38
43
  "coatepec": {
39
- "command": "bundle",
40
- "args": ["exec", "coatepec", "--root", "."]
44
+ "command": "coatepec",
45
+ "args": ["--root", "."]
41
46
  }
42
47
  }
43
48
  }
@@ -93,6 +98,29 @@ includes an `execution_mode` field (`fork`, `spawn_fallback`, or
93
98
  call; `spawn_after_crash` results also carry the crashed fork's own stderr
94
99
  under `crashed_fork_stderr` so the crash can be diagnosed.
95
100
 
101
+ `macos_fork: true` also effectively requires
102
+ `OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES` in Coatepec's own environment.
103
+ Without it, a forked child that touches an Objective-C-initialized class
104
+ aborts -- Coatepec retries via spawn, so it degrades silently to the slow
105
+ path (no crash, no error surfaced) rather than failing loudly, and you
106
+ simply never get the speedup. Set it on the MCP server process itself:
107
+
108
+ ```json
109
+ {
110
+ "mcpServers": {
111
+ "coatepec": {
112
+ "command": "coatepec",
113
+ "args": ["--root", "."],
114
+ "env": { "OBJC_DISABLE_INITIALIZE_FORK_SAFETY": "YES" }
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ Be aware this flag disables a real safety check in Apple's Objective-C
121
+ runtime; it's a reasonable trade for a local dev/test sidecar, but it's not
122
+ a no-op.
123
+
96
124
  ## Tools
97
125
 
98
126
  | Tool | Input | Notes |
@@ -102,6 +130,31 @@ under `crashed_fork_stderr` so the crash can be diagnosed.
102
130
  | `rails_routes` | `query?`, `limit?` (1..200, default 50), `offset?` | Case-insensitive filter across name/verb/path/controller/action |
103
131
  | `rails_model` | `name` (constant path, e.g. `Widget` or `Admin::Widget`) | ActiveRecord models only; columns, associations, validators, enums -- no row data |
104
132
 
133
+ ### Example queries
134
+
135
+ `rails_routes`:
136
+
137
+ - "What are all the routes in this app?" -- `rails_routes()`
138
+ - "What's the URL for widgets?" -- `rails_routes(query: "widget")`. `query` is a
139
+ case-insensitive substring match across name, verb, path, controller, *and*
140
+ action -- not just the path -- so a resource name alone typically returns
141
+ every route for that resource (index/create/new/...); narrow further with
142
+ something like `query: "new_widget"` to hit one route by name.
143
+ - "Which routes accept POST?" -- `rails_routes(query: "POST")`, the same
144
+ substring match applied to the verb column.
145
+
146
+ `rails_model`:
147
+
148
+ - "What columns does Widget have, and which are nullable?" --
149
+ `rails_model(name: "Widget")` -- see `columns[].null`, `columns[].sql_type`,
150
+ `columns[].default`.
151
+ - "What validations and associations does Widget enforce?" -- same call --
152
+ see `validators` and `associations`.
153
+ - "What happens if I ask about a non-model class, like a controller?" --
154
+ `rails_model(name: "ApplicationController")` raises `not_active_record_model`
155
+ rather than introspecting it (a nonexistent constant raises `model_not_found`
156
+ instead) -- the tool only ever reflects on `ActiveRecord::Base` descendants.
157
+
105
158
  The warm test worker forces Rails' reload-checking on for its own boot,
106
159
  regardless of the target app's own `test.rb` setting (which disables it by
107
160
  default) -- so editing a model file takes effect on the next tool call
@@ -115,6 +168,16 @@ worker). For most apps this is invisible, but if you ever see behavior differ
115
168
  between Coatepec and your own `bundle exec rspec`, this is the first thing to
116
169
  suspect.
117
170
 
171
+ ### Restarts
172
+
173
+ If any tool call fails with `sidecar_restart_required`, the target app's
174
+ `Gemfile`/`Gemfile.lock` changed since Coatepec's own parent process (the
175
+ "sidecar") started -- not just the warm test worker, which Coatepec restarts
176
+ on its own. This is expected any time you switch branches, pull, or rebase
177
+ across a commit that touches the Gemfile, since the sidecar is managed by
178
+ your MCP client rather than by Coatepec itself: restart your MCP client (or
179
+ however it manages the Coatepec process) to pick up the change.
180
+
118
181
  ## Security boundary
119
182
 
120
183
  No eval, console, SQL/record access, shell, Rake, or file-write tool. Spec
@@ -93,7 +93,32 @@ module Coatepec
93
93
  end
94
94
 
95
95
  def associations_for(klass)
96
- klass.reflect_on_all_associations.first(MAX_ITEMS).map { |assoc| build_association_data(assoc) }
96
+ klass.reflect_on_all_associations.first(MAX_ITEMS).map { |assoc| safe_association_data(assoc) }
97
+ end
98
+
99
+ # build_association_data's own field-level rescues (association_class_name,
100
+ # association_foreign_key) cover every failure mode seen in practice so
101
+ # far, but ActiveRecord's reflection internals are large enough that
102
+ # betting the whole rails_model call on having anticipated all of them
103
+ # is optimistic -- a has_one/has_many :through a polymorphic belongs_to
104
+ # is exactly the kind of case that wasn't anticipated until it crashed
105
+ # this method outright (see association_foreign_key). This is the
106
+ # boundary of last resort: one association's introspection failing
107
+ # degrades just that entry instead of the whole model. It deliberately
108
+ # does not rescue StandardError -- a NoMethodError here is a genuine
109
+ # Coatepec bug (e.g. a typo), and letting that crash loudly beats
110
+ # silently reporting it as "this association is fine, no data".
111
+ def safe_association_data(assoc)
112
+ build_association_data(assoc)
113
+ rescue NameError, ArgumentError, ::ActiveRecord::ActiveRecordError => e
114
+ degraded_association_data(assoc, e)
115
+ end
116
+
117
+ def degraded_association_data(assoc, error)
118
+ {
119
+ name: assoc.name.to_s, macro: assoc.macro.to_s, class_name: nil, foreign_key: nil, through: nil,
120
+ polymorphic: nil, error: "#{error.class}: #{error.message}"
121
+ }
97
122
  end
98
123
 
99
124
  def build_association_data(assoc)
@@ -101,7 +126,7 @@ module Coatepec
101
126
  name: assoc.name.to_s,
102
127
  macro: assoc.macro.to_s,
103
128
  class_name: association_class_name(assoc),
104
- foreign_key: assoc.foreign_key.to_s,
129
+ foreign_key: association_foreign_key(assoc),
105
130
  through: assoc.through_reflection&.name&.to_s,
106
131
  polymorphic: assoc.polymorphic? || false
107
132
  }
@@ -120,6 +145,20 @@ module Coatepec
120
145
  nil
121
146
  end
122
147
 
148
+ # A has_one/has_many :through reflection whose `through:` target is
149
+ # itself a polymorphic belongs_to has no single fixed class either --
150
+ # ThroughReflection#foreign_key needs through_reflection.klass to find
151
+ # the source reflection, and .klass on a polymorphic reflection always
152
+ # raises ArgumentError (see association_class_name above). A dangling
153
+ # class_name on the reflection itself still raises NameError the same
154
+ # way. Report foreign_key: nil rather than letting either crash the
155
+ # call.
156
+ def association_foreign_key(assoc)
157
+ assoc.foreign_key.to_s
158
+ rescue NameError, ArgumentError
159
+ nil
160
+ end
161
+
123
162
  def validators_for(klass)
124
163
  klass.validators.first(MAX_ITEMS).map do |validator|
125
164
  {
data/lib/coatepec/mcp.rb CHANGED
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mcp"
3
+ begin
4
+ require "mcp"
5
+ rescue LoadError
6
+ raise LoadError, "The \"mcp\" gem is required to run Coatepec but is not installed. " \
7
+ "Install it alongside Coatepec: `gem install coatepec mcp`."
8
+ end
9
+
4
10
  require "coatepec"
5
11
  require_relative "mcp/response"
6
12
  require_relative "mcp/tools"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coatepec
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.1"
5
5
  end
@@ -68,7 +68,10 @@ module Coatepec
68
68
  reason = @change_detector.restart_reason(@snapshot)
69
69
  case reason
70
70
  when :sidecar_restart_required
71
- raise Coatepec::Error.new(:sidecar_restart_required, "Gemfile changed; restart Coatepec")
71
+ raise Coatepec::Error.new(
72
+ :sidecar_restart_required,
73
+ "Gemfile changed; restart your MCP client to restart Coatepec and pick up the change"
74
+ )
72
75
  when :worker_restart_required
73
76
  restart_worker!
74
77
  end
metadata CHANGED
@@ -1,28 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coatepec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Enrique Mogollan
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-08-03 00:00:00.000000000 Z
10
+ date: 2026-08-07 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: mcp
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '1.0'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: '1.0'
26
12
  - !ruby/object:Gem::Dependency
27
13
  name: railties
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -43,6 +29,20 @@ dependencies:
43
29
  - - "<"
44
30
  - !ruby/object:Gem::Version
45
31
  version: '8.2'
32
+ - !ruby/object:Gem::Dependency
33
+ name: mcp
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '1.0'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '1.0'
46
46
  description: Coatepec is a local stdio MCP sidecar that keeps an isolated Rails test
47
47
  worker warm so coding agents can run targeted RSpec examples quickly, without exposing
48
48
  a general Rails console.