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 +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +76 -13
- data/lib/coatepec/introspection/model.rb +41 -2
- data/lib/coatepec/mcp.rb +7 -1
- data/lib/coatepec/version.rb +1 -1
- data/lib/coatepec/worker_manager.rb +4 -1
- metadata +16 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 161aafede82c7279701f75a51cc7976a6ef7bbbadff7bc22a7bc04dc050e1780
|
|
4
|
+
data.tar.gz: 2b867df0d495bb573e1e00ad9a94f9aca800fa7045bbf6965fb0825acfc2683f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
18
|
-
|
|
13
|
+
gem install coatepec mcp
|
|
14
|
+
coatepec --version
|
|
19
15
|
```
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
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 --
|
|
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": "
|
|
40
|
-
"args": ["
|
|
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|
|
|
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
|
|
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
|
-
|
|
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"
|
data/lib/coatepec/version.rb
CHANGED
|
@@ -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(
|
|
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
|
+
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-
|
|
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.
|