flehmen 0.1.0
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/LICENSE.txt +21 -0
- data/README.md +211 -0
- data/bin/flehmen +15 -0
- data/lib/flehmen/configuration.rb +26 -0
- data/lib/flehmen/field_filter.rb +33 -0
- data/lib/flehmen/model_registry.rb +94 -0
- data/lib/flehmen/query_builder.rb +77 -0
- data/lib/flehmen/resources/schema_overview_resource.rb +33 -0
- data/lib/flehmen/serializer.rb +18 -0
- data/lib/flehmen/tools/base.rb +13 -0
- data/lib/flehmen/tools/count_records_tool.rb +39 -0
- data/lib/flehmen/tools/describe_model_tool.rb +37 -0
- data/lib/flehmen/tools/find_record_tool.rb +33 -0
- data/lib/flehmen/tools/list_models_tool.rb +32 -0
- data/lib/flehmen/tools/search_records_tool.rb +54 -0
- data/lib/flehmen/tools/show_associations_tool.rb +77 -0
- data/lib/flehmen/version.rb +5 -0
- data/lib/flehmen.rb +89 -0
- metadata +114 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7e9d8368a8bcdfff9fca48d58621bf80fa261f0ca8a1999d668db8a96f707ece
|
|
4
|
+
data.tar.gz: 73121618744a5f85395c3b7eff399647e58625db1923245c40c88542fd4a4ab0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '01999308497ea1ab463e7347ee8f0c5ddb4ba8a7717ecc59925b010b81c8a898458ff5ed0d870532c831dc9d84c9836bb6e2cdb6f3d19fa4bb98d89fbb162ff8'
|
|
7
|
+
data.tar.gz: 95817db20d7b8d699b4b5ce42bf9c13e57fd75b847db0afdb95dbec40ba1f5100ee89b3e07fc4546d3ea7f3b6c4d3dec8ec4490f38ef939d61660d2370822802
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ryosk7
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Flehmen
|
|
2
|
+
|
|
3
|
+
<img width="410" height="410" alt="flehmen-response-cat" src="https://github.com/user-attachments/assets/07d5848a-d35f-4046-9530-69c855cfd07e" />
|
|
4
|
+
|
|
5
|
+
A Ruby gem that exposes Rails ActiveRecord models to Claude Desktop via the Model Context Protocol (MCP). It auto-discovers models, provides read-only query tools, and filters sensitive fields.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "flehmen", github: "ryosk7/flehmen"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
### Rails initializer
|
|
22
|
+
|
|
23
|
+
Create `config/initializers/flehmen.rb`:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
Flehmen.configure do |config|
|
|
27
|
+
config.exclude_models = []
|
|
28
|
+
config.sensitive_fields = %i[
|
|
29
|
+
password_digest encrypted_password token secret
|
|
30
|
+
api_key api_secret access_token refresh_token
|
|
31
|
+
otp_secret reset_password_token encrypted_phone
|
|
32
|
+
]
|
|
33
|
+
config.max_results = 100
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Flehmen.mount_in_rails(Rails.application, path_prefix: "/mcp", localhost_only: false)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Claude Desktop
|
|
40
|
+
|
|
41
|
+
Add to `claude_desktop_config.json`:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"flehmen": {
|
|
47
|
+
"command": "npx",
|
|
48
|
+
"args": ["mcp-remote", "http://localhost:3000/mcp/sse", "--allow-http"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> **Note:** The URL must end with `/mcp/sse`. The underlying `fast_mcp` gem uses SSE transport, so `/mcp` alone will return 404.
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
| Option | Type | Default | Description |
|
|
59
|
+
|---|---|---|---|
|
|
60
|
+
| `models` | `:all` or `Array` | `:all` | Models to expose. `:all` auto-discovers all `ApplicationRecord` descendants |
|
|
61
|
+
| `exclude_models` | `Array` | `[]` | Model classes or strings to exclude |
|
|
62
|
+
| `sensitive_fields` | `Array<Symbol>` | See below | Fields masked with `[FILTERED]` across all models |
|
|
63
|
+
| `model_sensitive_fields` | `Hash` | `{}` | Per-model sensitive fields |
|
|
64
|
+
| `max_results` | `Integer` | `100` | Maximum records returned by any query |
|
|
65
|
+
| `read_only_connection` | `Boolean` | `true` | Wraps all queries in `while_preventing_writes` to block accidental writes at the Rails level |
|
|
66
|
+
|
|
67
|
+
### Default sensitive fields
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
%i[
|
|
71
|
+
password_digest encrypted_password token secret
|
|
72
|
+
api_key api_secret access_token refresh_token
|
|
73
|
+
otp_secret reset_password_token confirmation_token
|
|
74
|
+
unlock_token remember_token authentication_token
|
|
75
|
+
]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Example
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
Flehmen.configure do |config|
|
|
82
|
+
config.exclude_models = [AdminUser, "InternalLog"]
|
|
83
|
+
config.sensitive_fields += [:ssn, :credit_card_number]
|
|
84
|
+
config.model_sensitive_fields = {
|
|
85
|
+
"User" => [:phone_number, :address]
|
|
86
|
+
}
|
|
87
|
+
config.max_results = 50
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Tools
|
|
92
|
+
|
|
93
|
+
### flehmen_list_models
|
|
94
|
+
|
|
95
|
+
Lists all discovered models.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
Arguments: none
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
```json
|
|
103
|
+
[
|
|
104
|
+
{
|
|
105
|
+
"name": "User",
|
|
106
|
+
"table_name": "users",
|
|
107
|
+
"column_count": 15,
|
|
108
|
+
"association_count": 8,
|
|
109
|
+
"enum_count": 2
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### flehmen_describe_model
|
|
115
|
+
|
|
116
|
+
Returns schema details (columns, associations, enums) for a model.
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
Arguments:
|
|
120
|
+
model_name (required) - e.g. "User"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### flehmen_find_record
|
|
124
|
+
|
|
125
|
+
Finds a single record by primary key.
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
Arguments:
|
|
129
|
+
model_name (required)
|
|
130
|
+
id (required)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### flehmen_search_records
|
|
134
|
+
|
|
135
|
+
Searches records with structured conditions.
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
Arguments:
|
|
139
|
+
model_name (required)
|
|
140
|
+
conditions (optional) - JSON string
|
|
141
|
+
order_by (optional) - Column name to sort by
|
|
142
|
+
order_dir (optional) - "asc" or "desc" (default: "asc")
|
|
143
|
+
limit (optional)
|
|
144
|
+
offset (optional)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Conditions format:
|
|
148
|
+
```json
|
|
149
|
+
[
|
|
150
|
+
{"field": "status", "operator": "eq", "value": "active"},
|
|
151
|
+
{"field": "created_at", "operator": "gte", "value": "2025-01-01"}
|
|
152
|
+
]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Supported operators: `eq`, `not_eq`, `gt`, `gte`, `lt`, `lte`, `like`, `not_like`, `in`, `not_in`, `null`, `not_null`
|
|
156
|
+
|
|
157
|
+
### flehmen_count_records
|
|
158
|
+
|
|
159
|
+
Counts records matching conditions.
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
Arguments:
|
|
163
|
+
model_name (required)
|
|
164
|
+
conditions (optional) - Same format as search_records
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### flehmen_show_associations
|
|
168
|
+
|
|
169
|
+
Fetches associated records for a given record.
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
Arguments:
|
|
173
|
+
model_name (required)
|
|
174
|
+
id (required)
|
|
175
|
+
association_name (required) - e.g. "posts", "company"
|
|
176
|
+
limit (optional) - For has_many associations
|
|
177
|
+
offset (optional)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
## Resources
|
|
182
|
+
|
|
183
|
+
### flehmen://schema/overview
|
|
184
|
+
|
|
185
|
+
Returns a JSON overview of all models including columns, associations, and enums.
|
|
186
|
+
|
|
187
|
+
## Architecture
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
Flehmen.mount_in_rails(app)
|
|
191
|
+
└── FastMcp.mount_in_rails (Rack middleware)
|
|
192
|
+
├── GET /mcp/sse → SSE connection (keep-alive)
|
|
193
|
+
└── POST /mcp/messages → JSON-RPC message handling
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- **ModelRegistry** - Auto-discovers models on first access (lazy loading)
|
|
197
|
+
- **FieldFilter** - Masks sensitive fields with `[FILTERED]`
|
|
198
|
+
- **QueryBuilder** - Builds Arel-based queries from structured conditions (SQL injection safe)
|
|
199
|
+
- **Serializer** - Converts records to filtered hashes
|
|
200
|
+
|
|
201
|
+
## Dependencies
|
|
202
|
+
|
|
203
|
+
- Ruby >= 3.1.0
|
|
204
|
+
- `fast-mcp` ~> 1.5
|
|
205
|
+
- `activerecord` >= 7.0
|
|
206
|
+
- `activesupport` >= 7.0
|
|
207
|
+
- `railties` >= 7.0
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT
|
data/bin/flehmen
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
|
|
6
|
+
app_path = ENV["RAILS_APP_PATH"] || Dir.pwd
|
|
7
|
+
ENV["RAILS_ENV"] ||= "development"
|
|
8
|
+
|
|
9
|
+
require File.join(app_path, "config", "environment")
|
|
10
|
+
Rails.application.eager_load!
|
|
11
|
+
|
|
12
|
+
require "flehmen"
|
|
13
|
+
|
|
14
|
+
Flehmen.boot!
|
|
15
|
+
Flehmen.start_server!
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flehmen
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :models,
|
|
6
|
+
:exclude_models,
|
|
7
|
+
:sensitive_fields,
|
|
8
|
+
:model_sensitive_fields,
|
|
9
|
+
:max_results,
|
|
10
|
+
:read_only_connection
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@models = :all
|
|
14
|
+
@exclude_models = []
|
|
15
|
+
@sensitive_fields = %i[
|
|
16
|
+
password_digest encrypted_password token secret
|
|
17
|
+
api_key api_secret access_token refresh_token
|
|
18
|
+
otp_secret reset_password_token confirmation_token
|
|
19
|
+
unlock_token remember_token authentication_token
|
|
20
|
+
]
|
|
21
|
+
@model_sensitive_fields = {}
|
|
22
|
+
@max_results = 100
|
|
23
|
+
@read_only_connection = true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flehmen
|
|
4
|
+
class FieldFilter
|
|
5
|
+
FILTERED_PLACEHOLDER = "[FILTERED]"
|
|
6
|
+
|
|
7
|
+
def initialize(config = Flehmen.configuration)
|
|
8
|
+
@global_sensitive = config.sensitive_fields.map(&:to_s)
|
|
9
|
+
@model_sensitive = config.model_sensitive_fields.transform_keys(&:to_s)
|
|
10
|
+
.transform_values { |v| v.map(&:to_s) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def filter_attributes(model_name, attributes_hash)
|
|
14
|
+
sensitive = sensitive_fields_for(model_name)
|
|
15
|
+
attributes_hash.each_with_object({}) do |(k, v), filtered|
|
|
16
|
+
filtered[k] = sensitive.include?(k.to_s) ? FILTERED_PLACEHOLDER : v
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def visible_columns(model_name, all_columns)
|
|
21
|
+
sensitive = sensitive_fields_for(model_name)
|
|
22
|
+
all_columns.map do |col|
|
|
23
|
+
col.merge(sensitive: sensitive.include?(col[:name].to_s))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def sensitive_fields_for(model_name)
|
|
30
|
+
@global_sensitive + (@model_sensitive[model_name.to_s] || [])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flehmen
|
|
4
|
+
class ModelRegistry
|
|
5
|
+
attr_reader :models
|
|
6
|
+
|
|
7
|
+
def initialize(config = Flehmen.configuration)
|
|
8
|
+
@config = config
|
|
9
|
+
@models = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def discover!
|
|
13
|
+
base_class = defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base
|
|
14
|
+
raw_models = if @config.models == :all
|
|
15
|
+
base_class.descendants
|
|
16
|
+
else
|
|
17
|
+
@config.models.map { |m| m.is_a?(String) ? m.constantize : m }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
excluded = @config.exclude_models.map { |m| m.is_a?(String) ? m.constantize : m }
|
|
21
|
+
|
|
22
|
+
raw_models.each do |klass|
|
|
23
|
+
next if klass.abstract_class?
|
|
24
|
+
next if excluded.include?(klass)
|
|
25
|
+
next unless safe_table_exists?(klass)
|
|
26
|
+
|
|
27
|
+
register(klass)
|
|
28
|
+
rescue StandardError
|
|
29
|
+
# Skip models that fail introspection (e.g., STI subclasses with missing tables)
|
|
30
|
+
next
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def model_names
|
|
37
|
+
@models.keys.sort
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def find_model(name)
|
|
41
|
+
@models[name] || @models[name.to_s.classify]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def safe_table_exists?(klass)
|
|
47
|
+
klass.table_exists?
|
|
48
|
+
rescue StandardError
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def register(klass)
|
|
53
|
+
@models[klass.name] = {
|
|
54
|
+
klass: klass,
|
|
55
|
+
table_name: klass.table_name,
|
|
56
|
+
columns: extract_columns(klass),
|
|
57
|
+
associations: extract_associations(klass),
|
|
58
|
+
enums: extract_enums(klass),
|
|
59
|
+
primary_key: klass.primary_key
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def extract_columns(klass)
|
|
64
|
+
klass.columns.map do |col|
|
|
65
|
+
{
|
|
66
|
+
name: col.name,
|
|
67
|
+
type: col.type.to_s,
|
|
68
|
+
null: col.null,
|
|
69
|
+
default: col.default,
|
|
70
|
+
limit: col.limit
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def extract_associations(klass)
|
|
76
|
+
klass.reflect_on_all_associations.map do |assoc|
|
|
77
|
+
{
|
|
78
|
+
name: assoc.name.to_s,
|
|
79
|
+
type: assoc.macro.to_s,
|
|
80
|
+
class_name: assoc.class_name,
|
|
81
|
+
foreign_key: assoc.foreign_key.to_s,
|
|
82
|
+
through: assoc.options[:through]&.to_s,
|
|
83
|
+
polymorphic: assoc.options[:polymorphic] || false
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def extract_enums(klass)
|
|
89
|
+
return {} unless klass.respond_to?(:defined_enums)
|
|
90
|
+
|
|
91
|
+
klass.defined_enums.transform_values(&:keys)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flehmen
|
|
4
|
+
class QueryBuilder
|
|
5
|
+
ALLOWED_OPERATORS = %w[eq not_eq gt gte lt lte like not_like in not_in null not_null].freeze
|
|
6
|
+
|
|
7
|
+
def initialize(model_info, config = Flehmen.configuration)
|
|
8
|
+
@klass = model_info[:klass]
|
|
9
|
+
@column_names = model_info[:columns].map { |c| c[:name] }
|
|
10
|
+
@config = config
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def build(conditions: [], order_by: nil, order_dir: "asc", limit: nil, offset: nil)
|
|
14
|
+
scope = @klass.all
|
|
15
|
+
|
|
16
|
+
conditions.each do |cond|
|
|
17
|
+
scope = apply_condition(scope, cond)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
scope = apply_ordering(scope, order_by, order_dir)
|
|
21
|
+
scope = scope.limit(effective_limit(limit))
|
|
22
|
+
scope = scope.offset(offset.to_i) if offset && offset.to_i > 0
|
|
23
|
+
scope
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def apply_condition(scope, cond)
|
|
29
|
+
field = cond["field"]&.to_s || cond[:field]&.to_s
|
|
30
|
+
operator = cond["operator"]&.to_s || cond[:operator]&.to_s
|
|
31
|
+
value = cond["value"] || cond[:value]
|
|
32
|
+
|
|
33
|
+
raise ArgumentError, "Missing field in condition" if field.nil? || field.empty?
|
|
34
|
+
raise ArgumentError, "Missing operator in condition" if operator.nil? || operator.empty?
|
|
35
|
+
raise ArgumentError, "Unknown column: #{field}" unless @column_names.include?(field)
|
|
36
|
+
raise ArgumentError, "Unknown operator: #{operator}" unless ALLOWED_OPERATORS.include?(operator)
|
|
37
|
+
|
|
38
|
+
table = @klass.arel_table
|
|
39
|
+
|
|
40
|
+
case operator
|
|
41
|
+
when "eq" then scope.where(table[field].eq(value))
|
|
42
|
+
when "not_eq" then scope.where(table[field].not_eq(value))
|
|
43
|
+
when "gt" then scope.where(table[field].gt(value))
|
|
44
|
+
when "gte" then scope.where(table[field].gteq(value))
|
|
45
|
+
when "lt" then scope.where(table[field].lt(value))
|
|
46
|
+
when "lte" then scope.where(table[field].lteq(value))
|
|
47
|
+
when "like" then scope.where(table[field].matches(sanitize_like(value)))
|
|
48
|
+
when "not_like" then scope.where(table[field].does_not_match(sanitize_like(value)))
|
|
49
|
+
when "in" then scope.where(table[field].in(Array(value).first(@config.max_results)))
|
|
50
|
+
when "not_in" then scope.where(table[field].not_in(Array(value).first(@config.max_results)))
|
|
51
|
+
when "null" then scope.where(table[field].eq(nil))
|
|
52
|
+
when "not_null" then scope.where(table[field].not_eq(nil))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def apply_ordering(scope, order_by, order_dir)
|
|
57
|
+
return scope.order(id: :asc) unless order_by
|
|
58
|
+
|
|
59
|
+
raise ArgumentError, "Unknown column: #{order_by}" unless @column_names.include?(order_by.to_s)
|
|
60
|
+
|
|
61
|
+
dir = %w[asc desc].include?(order_dir.to_s.downcase) ? order_dir.to_s.downcase.to_sym : :asc
|
|
62
|
+
scope.order(order_by.to_sym => dir)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def effective_limit(requested)
|
|
66
|
+
max = @config.max_results
|
|
67
|
+
return max unless requested
|
|
68
|
+
|
|
69
|
+
[requested.to_i, max].min
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def sanitize_like(value)
|
|
73
|
+
# Escape special LIKE characters to prevent unintended wildcards
|
|
74
|
+
value.to_s.gsub("\\", "\\\\\\\\").gsub("%", "\\%").gsub("_", "\\_")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Flehmen
|
|
6
|
+
module Resources
|
|
7
|
+
class SchemaOverviewResource < FastMcp::Resource
|
|
8
|
+
uri "flehmen://schema/overview"
|
|
9
|
+
resource_name "Database Schema Overview"
|
|
10
|
+
description "Complete overview of all available models, their columns, associations, and enums"
|
|
11
|
+
mime_type "application/json"
|
|
12
|
+
|
|
13
|
+
def content
|
|
14
|
+
registry = Flehmen.model_registry
|
|
15
|
+
filter = Flehmen::FieldFilter.new
|
|
16
|
+
|
|
17
|
+
overview = registry.model_names.map do |name|
|
|
18
|
+
info = registry.find_model(name)
|
|
19
|
+
{
|
|
20
|
+
model: name,
|
|
21
|
+
table: info[:table_name],
|
|
22
|
+
primary_key: info[:primary_key],
|
|
23
|
+
columns: filter.visible_columns(name, info[:columns]),
|
|
24
|
+
associations: info[:associations],
|
|
25
|
+
enums: info[:enums]
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
JSON.pretty_generate(overview)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flehmen
|
|
4
|
+
class Serializer
|
|
5
|
+
def initialize(field_filter = FieldFilter.new)
|
|
6
|
+
@field_filter = field_filter
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def serialize_record(record)
|
|
10
|
+
model_name = record.class.name
|
|
11
|
+
@field_filter.filter_attributes(model_name, record.attributes)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def serialize_records(records)
|
|
15
|
+
records.map { |r| serialize_record(r) }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flehmen
|
|
4
|
+
module Tools
|
|
5
|
+
class Base < FastMcp::Tool
|
|
6
|
+
def call(**args)
|
|
7
|
+
ActiveRecord::Base.while_preventing_writes(Flehmen.configuration.read_only_connection) do
|
|
8
|
+
execute(**args)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Flehmen
|
|
6
|
+
module Tools
|
|
7
|
+
class CountRecordsTool < Base
|
|
8
|
+
tool_name "flehmen_count_records"
|
|
9
|
+
description 'Count records matching filter conditions. Example conditions: [{"field":"status","operator":"eq","value":"active"}]'
|
|
10
|
+
|
|
11
|
+
arguments do
|
|
12
|
+
required(:model_name).filled(:string).description("Name of the model class")
|
|
13
|
+
optional(:conditions).filled(:string).description('JSON array of conditions: [{"field":"...", "operator":"...", "value":"..."}]')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
annotations(
|
|
17
|
+
read_only_hint: true,
|
|
18
|
+
open_world_hint: false
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def execute(model_name:, conditions: nil)
|
|
22
|
+
info = Flehmen.model_registry.find_model(model_name)
|
|
23
|
+
return JSON.generate({ error: "Model not found: #{model_name}" }) unless info
|
|
24
|
+
|
|
25
|
+
parsed_conditions = conditions ? JSON.parse(conditions) : []
|
|
26
|
+
|
|
27
|
+
builder = Flehmen::QueryBuilder.new(info)
|
|
28
|
+
scope = builder.build(conditions: parsed_conditions, limit: nil)
|
|
29
|
+
count = scope.unscope(:limit, :offset, :order).count
|
|
30
|
+
|
|
31
|
+
JSON.generate({ model: model_name, count: count })
|
|
32
|
+
rescue JSON::ParserError
|
|
33
|
+
JSON.generate({ error: "Invalid JSON in conditions parameter" })
|
|
34
|
+
rescue ArgumentError => e
|
|
35
|
+
JSON.generate({ error: e.message })
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Flehmen
|
|
6
|
+
module Tools
|
|
7
|
+
class DescribeModelTool < Base
|
|
8
|
+
tool_name "flehmen_describe_model"
|
|
9
|
+
description "Show the full schema for a model: columns (name, type, null, default), associations (name, type, target class), and enum definitions"
|
|
10
|
+
|
|
11
|
+
arguments do
|
|
12
|
+
required(:model_name).filled(:string).description("Name of the model class, e.g. 'User' or 'Post'")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
annotations(
|
|
16
|
+
read_only_hint: true,
|
|
17
|
+
open_world_hint: false
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def execute(model_name:)
|
|
21
|
+
info = Flehmen.model_registry.find_model(model_name)
|
|
22
|
+
return JSON.generate({ error: "Model not found: #{model_name}" }) unless info
|
|
23
|
+
|
|
24
|
+
filter = Flehmen::FieldFilter.new
|
|
25
|
+
result = {
|
|
26
|
+
model: model_name,
|
|
27
|
+
table_name: info[:table_name],
|
|
28
|
+
primary_key: info[:primary_key],
|
|
29
|
+
columns: filter.visible_columns(model_name, info[:columns]),
|
|
30
|
+
associations: info[:associations],
|
|
31
|
+
enums: info[:enums]
|
|
32
|
+
}
|
|
33
|
+
JSON.generate(result)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Flehmen
|
|
6
|
+
module Tools
|
|
7
|
+
class FindRecordTool < Base
|
|
8
|
+
tool_name "flehmen_find_record"
|
|
9
|
+
description "Find a single record by its primary key (usually ID)"
|
|
10
|
+
|
|
11
|
+
arguments do
|
|
12
|
+
required(:model_name).filled(:string).description("Name of the model class")
|
|
13
|
+
required(:id).filled(:string).description("Primary key value of the record")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
annotations(
|
|
17
|
+
read_only_hint: true,
|
|
18
|
+
open_world_hint: false
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def execute(model_name:, id:)
|
|
22
|
+
info = Flehmen.model_registry.find_model(model_name)
|
|
23
|
+
return JSON.generate({ error: "Model not found: #{model_name}" }) unless info
|
|
24
|
+
|
|
25
|
+
record = info[:klass].find_by(info[:primary_key] => id)
|
|
26
|
+
return JSON.generate({ error: "Record not found: #{model_name}##{id}" }) unless record
|
|
27
|
+
|
|
28
|
+
serializer = Flehmen::Serializer.new
|
|
29
|
+
JSON.generate(serializer.serialize_record(record))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Flehmen
|
|
6
|
+
module Tools
|
|
7
|
+
class ListModelsTool < Base
|
|
8
|
+
tool_name "flehmen_list_models"
|
|
9
|
+
description "List all available ActiveRecord models with their table names, column counts, and association counts"
|
|
10
|
+
|
|
11
|
+
annotations(
|
|
12
|
+
read_only_hint: true,
|
|
13
|
+
open_world_hint: false
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def execute(**_args)
|
|
17
|
+
registry = Flehmen.model_registry
|
|
18
|
+
models = registry.model_names.map do |name|
|
|
19
|
+
info = registry.find_model(name)
|
|
20
|
+
{
|
|
21
|
+
name: name,
|
|
22
|
+
table_name: info[:table_name],
|
|
23
|
+
column_count: info[:columns].size,
|
|
24
|
+
association_count: info[:associations].size,
|
|
25
|
+
enum_count: info[:enums].size
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
JSON.generate(models)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Flehmen
|
|
6
|
+
module Tools
|
|
7
|
+
class SearchRecordsTool < Base
|
|
8
|
+
tool_name "flehmen_search_records"
|
|
9
|
+
description 'Search records with filter conditions. Each condition has a field, operator (eq, not_eq, gt, gte, lt, lte, like, in, null, not_null), and value. Example conditions: [{"field":"status","operator":"eq","value":"active"}]'
|
|
10
|
+
|
|
11
|
+
arguments do
|
|
12
|
+
required(:model_name).filled(:string).description("Name of the model class")
|
|
13
|
+
optional(:conditions).filled(:string).description('JSON array of conditions: [{"field":"...", "operator":"...", "value":"..."}]')
|
|
14
|
+
optional(:order_by).filled(:string).description("Column name to order by")
|
|
15
|
+
optional(:order_dir).filled(:string).description("Order direction: 'asc' or 'desc'")
|
|
16
|
+
optional(:limit).filled(:integer).value(gteq?: 1, lteq?: 100).description("Max records to return (capped by server config)")
|
|
17
|
+
optional(:offset).filled(:integer).value(gteq?: 0, lteq?: 10000).description("Number of records to skip for pagination")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
annotations(
|
|
21
|
+
read_only_hint: true,
|
|
22
|
+
open_world_hint: false
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def execute(model_name:, conditions: nil, order_by: nil, order_dir: "asc", limit: nil, offset: nil)
|
|
26
|
+
info = Flehmen.model_registry.find_model(model_name)
|
|
27
|
+
return JSON.generate({ error: "Model not found: #{model_name}" }) unless info
|
|
28
|
+
|
|
29
|
+
parsed_conditions = conditions ? JSON.parse(conditions) : []
|
|
30
|
+
|
|
31
|
+
builder = Flehmen::QueryBuilder.new(info)
|
|
32
|
+
records = builder.build(
|
|
33
|
+
conditions: parsed_conditions,
|
|
34
|
+
order_by: order_by,
|
|
35
|
+
order_dir: order_dir,
|
|
36
|
+
limit: limit,
|
|
37
|
+
offset: offset
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
serializer = Flehmen::Serializer.new
|
|
41
|
+
result = {
|
|
42
|
+
model: model_name,
|
|
43
|
+
count: records.size,
|
|
44
|
+
records: serializer.serialize_records(records)
|
|
45
|
+
}
|
|
46
|
+
JSON.generate(result)
|
|
47
|
+
rescue JSON::ParserError
|
|
48
|
+
JSON.generate({ error: "Invalid JSON in conditions parameter" })
|
|
49
|
+
rescue ArgumentError => e
|
|
50
|
+
JSON.generate({ error: e.message })
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Flehmen
|
|
6
|
+
module Tools
|
|
7
|
+
class ShowAssociationsTool < Base
|
|
8
|
+
tool_name "flehmen_show_associations"
|
|
9
|
+
description "Navigate a record's associations. Given a model, record ID, and association name, returns the associated records."
|
|
10
|
+
|
|
11
|
+
arguments do
|
|
12
|
+
required(:model_name).filled(:string).description("Name of the source model class")
|
|
13
|
+
required(:id).filled(:string).description("Primary key of the source record")
|
|
14
|
+
required(:association_name).filled(:string).description("Name of the association to navigate")
|
|
15
|
+
optional(:limit).filled(:integer).value(gteq?: 1, lteq?: 100).description("Max associated records to return")
|
|
16
|
+
optional(:offset).filled(:integer).value(gteq?: 0, lteq?: 10000).description("Number of records to skip")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
annotations(
|
|
20
|
+
read_only_hint: true,
|
|
21
|
+
open_world_hint: false
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def execute(model_name:, id:, association_name:, limit: nil, offset: nil)
|
|
25
|
+
info = Flehmen.model_registry.find_model(model_name)
|
|
26
|
+
return JSON.generate({ error: "Model not found: #{model_name}" }) unless info
|
|
27
|
+
|
|
28
|
+
# Validate association name against declared associations
|
|
29
|
+
valid_associations = info[:associations].map { |a| a[:name] }
|
|
30
|
+
unless valid_associations.include?(association_name)
|
|
31
|
+
return JSON.generate({ error: "Unknown association: #{association_name}" })
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
record = info[:klass].find_by(info[:primary_key] => id)
|
|
35
|
+
return JSON.generate({ error: "Record not found: #{model_name}##{id}" }) unless record
|
|
36
|
+
|
|
37
|
+
assoc_meta = info[:associations].find { |a| a[:name] == association_name }
|
|
38
|
+
associated = record.public_send(association_name)
|
|
39
|
+
|
|
40
|
+
serializer = Flehmen::Serializer.new
|
|
41
|
+
max = Flehmen.configuration.max_results
|
|
42
|
+
|
|
43
|
+
if %w[has_many has_and_belongs_to_many].include?(assoc_meta[:type])
|
|
44
|
+
effective_limit = limit ? [limit.to_i, max].min : max
|
|
45
|
+
scope = associated.limit(effective_limit)
|
|
46
|
+
scope = scope.offset(offset.to_i) if offset && offset.to_i > 0
|
|
47
|
+
records = scope.to_a
|
|
48
|
+
|
|
49
|
+
JSON.generate({
|
|
50
|
+
source: "#{model_name}##{id}",
|
|
51
|
+
association: association_name,
|
|
52
|
+
type: assoc_meta[:type],
|
|
53
|
+
count: records.size,
|
|
54
|
+
records: serializer.serialize_records(records)
|
|
55
|
+
})
|
|
56
|
+
else
|
|
57
|
+
# belongs_to / has_one
|
|
58
|
+
if associated
|
|
59
|
+
JSON.generate({
|
|
60
|
+
source: "#{model_name}##{id}",
|
|
61
|
+
association: association_name,
|
|
62
|
+
type: assoc_meta[:type],
|
|
63
|
+
record: serializer.serialize_record(associated)
|
|
64
|
+
})
|
|
65
|
+
else
|
|
66
|
+
JSON.generate({
|
|
67
|
+
source: "#{model_name}##{id}",
|
|
68
|
+
association: association_name,
|
|
69
|
+
type: assoc_meta[:type],
|
|
70
|
+
record: nil
|
|
71
|
+
})
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/flehmen.rb
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fast_mcp"
|
|
4
|
+
require_relative "flehmen/version"
|
|
5
|
+
require_relative "flehmen/configuration"
|
|
6
|
+
require_relative "flehmen/model_registry"
|
|
7
|
+
require_relative "flehmen/field_filter"
|
|
8
|
+
require_relative "flehmen/query_builder"
|
|
9
|
+
require_relative "flehmen/serializer"
|
|
10
|
+
require_relative "flehmen/tools/base"
|
|
11
|
+
require_relative "flehmen/tools/list_models_tool"
|
|
12
|
+
require_relative "flehmen/tools/describe_model_tool"
|
|
13
|
+
require_relative "flehmen/tools/find_record_tool"
|
|
14
|
+
require_relative "flehmen/tools/search_records_tool"
|
|
15
|
+
require_relative "flehmen/tools/count_records_tool"
|
|
16
|
+
require_relative "flehmen/tools/show_associations_tool"
|
|
17
|
+
require_relative "flehmen/resources/schema_overview_resource"
|
|
18
|
+
|
|
19
|
+
module Flehmen
|
|
20
|
+
class << self
|
|
21
|
+
attr_writer :configuration
|
|
22
|
+
|
|
23
|
+
def configuration
|
|
24
|
+
@configuration ||= Configuration.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def configure
|
|
28
|
+
yield(configuration)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reset_configuration!
|
|
32
|
+
@configuration = Configuration.new
|
|
33
|
+
@model_registry = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Lazily discover models on first access
|
|
37
|
+
def model_registry
|
|
38
|
+
@model_registry || boot!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def boot!
|
|
42
|
+
@model_registry = ModelRegistry.new(configuration)
|
|
43
|
+
@model_registry.discover!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def start_server!
|
|
47
|
+
server = build_server
|
|
48
|
+
server.start
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def mount_in_rails(app, options = {})
|
|
52
|
+
opts = {
|
|
53
|
+
name: "flehmen",
|
|
54
|
+
version: Flehmen::VERSION,
|
|
55
|
+
path_prefix: options.delete(:path_prefix) || "/mcp"
|
|
56
|
+
}.merge(options)
|
|
57
|
+
|
|
58
|
+
FastMcp.mount_in_rails(app, opts) do |server|
|
|
59
|
+
register_tools(server)
|
|
60
|
+
register_resources(server)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def build_server
|
|
67
|
+
server = FastMcp::Server.new(
|
|
68
|
+
name: "flehmen",
|
|
69
|
+
version: Flehmen::VERSION
|
|
70
|
+
)
|
|
71
|
+
register_tools(server)
|
|
72
|
+
register_resources(server)
|
|
73
|
+
server
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def register_tools(server)
|
|
77
|
+
server.register_tool(Tools::ListModelsTool)
|
|
78
|
+
server.register_tool(Tools::DescribeModelTool)
|
|
79
|
+
server.register_tool(Tools::FindRecordTool)
|
|
80
|
+
server.register_tool(Tools::SearchRecordsTool)
|
|
81
|
+
server.register_tool(Tools::CountRecordsTool)
|
|
82
|
+
server.register_tool(Tools::ShowAssociationsTool)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def register_resources(server)
|
|
86
|
+
server.register_resource(Resources::SchemaOverviewResource)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: flehmen
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ryosk7
|
|
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: fast-mcp
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.5'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.5'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activerecord
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: activesupport
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '7.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '7.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: railties
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '7.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '7.0'
|
|
68
|
+
description: A generic Ruby gem that auto-discovers ActiveRecord models and provides
|
|
69
|
+
read-only query tools via the Model Context Protocol (MCP) for Claude Desktop integration.
|
|
70
|
+
executables:
|
|
71
|
+
- flehmen
|
|
72
|
+
extensions: []
|
|
73
|
+
extra_rdoc_files: []
|
|
74
|
+
files:
|
|
75
|
+
- LICENSE.txt
|
|
76
|
+
- README.md
|
|
77
|
+
- bin/flehmen
|
|
78
|
+
- lib/flehmen.rb
|
|
79
|
+
- lib/flehmen/configuration.rb
|
|
80
|
+
- lib/flehmen/field_filter.rb
|
|
81
|
+
- lib/flehmen/model_registry.rb
|
|
82
|
+
- lib/flehmen/query_builder.rb
|
|
83
|
+
- lib/flehmen/resources/schema_overview_resource.rb
|
|
84
|
+
- lib/flehmen/serializer.rb
|
|
85
|
+
- lib/flehmen/tools/base.rb
|
|
86
|
+
- lib/flehmen/tools/count_records_tool.rb
|
|
87
|
+
- lib/flehmen/tools/describe_model_tool.rb
|
|
88
|
+
- lib/flehmen/tools/find_record_tool.rb
|
|
89
|
+
- lib/flehmen/tools/list_models_tool.rb
|
|
90
|
+
- lib/flehmen/tools/search_records_tool.rb
|
|
91
|
+
- lib/flehmen/tools/show_associations_tool.rb
|
|
92
|
+
- lib/flehmen/version.rb
|
|
93
|
+
homepage: https://github.com/ryosk7/flehmen
|
|
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.1.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.7
|
|
112
|
+
specification_version: 4
|
|
113
|
+
summary: MCP server gem that exposes Rails ActiveRecord models to Claude Desktop
|
|
114
|
+
test_files: []
|