All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
1.7.0 - 2026-07-31
Read this if you're upgrading. This release fixes the defects found in a real-world integration shakedown of 1.6.0. The headline changes:
- Tool responses are now JSON instead of
inspect/1output (no more Elixir struct syntax, no more silently truncated lists).- Batch operations actually work over MCP (previously they silently reported
total: 0), andinclude/preloadableactually preloads.- Remote callers can no longer exhaust the BEAM atom table via
order_byor filter parameters.- The documented supervision snippet boots.
anubis_mcpis now pinned to~> 1.14,Ectomancer.child_spec/2produces a working entry, andforward "/mcp", Ectomancer.Plug, ...compiles in routers.only:/except:redact read results. Excluded fields no longer leak inlist/get/batch output.- New
scope:option for multi-tenant row-level scoping.No breaking changes in this release.
Added
scope:option forexpose— row-level scoping for multi-tenant apps. The function receives the query and the authenticated actor (fn query, actor -> query end) and is applied to every generated CRUD query. Composes with authorization-policy scopes (#140).- Configurable query limit ceiling —
config :ectomancer, max_limit: N(default100). The effectivelimitis reported in pagination metadata so clamping is visible (#150).
Changed
- anubis_mcp requirement tightened to
~> 1.14(was~> 1.5). The old range resolved the current release anyway; the constraint now reflects what is actually tested. Ectomancer.child_spec/2produces a working supervision entry. The old output{Anubis.Server.Supervisor, {server, transport: ...}}called the non-existentstart_link/1and failed at boot (#143, #148). It now returns a single{server, transport: {transport, start: true}}entry — resolved through the server module's ownchild_spec/1— sochildren = [Ectomancer.child_spec(MyApp.MCP, transports: [:streamable_http]), MyAppWeb.Endpoint]boots as documented. One transport per server module is supported; requesting two raises a clear error.- Router mounting works on anubis 1.14.
Ectomancer.Plug.init/1passes an escapablesubscriber_metadataremote capture soforward "/mcp", Ectomancer.Plug, ...compiles in Phoenix/Plug routers instead of failing with "cannot inject attribute @plug_forward_opts" (#143). - README +
Ectomancer.Plugdocs updated to the working supervision form.
Fixed
- Tool results are now JSON, not
inspect/1output (#147). Responses are serialized through a sanitizer that converts Ecto structs to plain maps, drops__meta__, replacesNotLoadedwithnull, and renders datetimes as ISO-8601.inspect/1's silent 50-row truncation is gone, and error responses no longer echo full stacktraces. - Batch operations no longer silently no-op (#145). Anubis delivers params with atom keys; the batch handlers read string keys, so
batch_createreportedtotal: 0. Param keys are now normalized once at the tool-execution funnel, andRepo.batch_*read both key shapes. include/preloadableactually preloads (#146). Theincludeparam is honored after the key-normalization fix, andvalidate_includes/3no longer crashes on string allowlists (removed the dead:allclause that also did unboundedString.to_atom).- Batch operations isolate per-record failures (#153). Each per-item write runs in a savepoint so a database-level constraint violation cannot poison the surrounding transaction; the documented partial-failure semantics are kept and the README no longer claims batches are atomic.
- No unbounded atom creation from remote input (#142).
order_by, filter keys, and param names wereString.to_atom'd from caller-influenced strings before the allowlist check, letting an unauthenticated client exhaust the BEAM atom table. Filtering now resolves against the schema's field allowlist by string comparison and only ever references existing atoms. exposecompiles on schemas withhas_throughassociations (#144).get_associations/1readassoc.relatedunconditionally, butEcto.Association.HasThroughhas no:relatedkey — any schema with ahas_many/has_one ... :throughfailed at compile time withKeyError. Falls back to the owning schema.- Correct plural tool names (#149).
list/batch tool names appended a literal"s", producinglist_studys,list_statuss. They now usePlurality.pluralize/1(list_studies,list_statuses,list_news). - README/docs
authorize: with:syntax errors fixed (#151).use Ectomancer, authorize: with: Moduleandexpose ..., authorize: with: Moduleare invalid Elixir; the docs now showauthorize: Module. - Playground + demo assets ship in the hex package (#152).
priv/(playground HTML, demo GIF, demo cast) was missing frommix.exsfiles:, so README references were broken for hex installs. only:/except:now redact read results — excluded fields are stripped from every row returned bylist,get, and batch tools (previously they only affected input params and resource metadata) (#141).- Tool name pluralization —
singularize_resource/1now usesPluralityfor noun inflection and keeps already-singular words ending in "s" intact. Tool names forstatus,analysis,business,series,class,address, andnewsare no longer truncated (get_statusinstead ofget_statu, etc.) (#128) - Compile warnings — grouped
build_assoc_params/1clauses and silenced the unusedassocparameter inchild_fk/3so the lint CI job (--warnings-as-errors) passes.
Testing
- 874 tests across CRUD, filtering, batch, preload, scope, redaction, auth, supervision, and atom-safety suites.
- Verified end-to-end: fresh Phoenix 1.8.9 app (SQLite, Elixir 1.20/OTP 29) installing from git, mounted at
/mcp, driven over MCP Streamable HTTP — scope isolation,except:redaction, JSON output, batch/include, plural tool names, and boot with the documented supervision pattern all confirmed. - CI matrix: Elixir 1.18.4–1.20.0 / OTP 26–29, with format, Credo, and Dialyzer on the lint job.
Issues Closed
- #140 — No tenant scoping by default (fixed via
scope:option) - #141 —
except:does not redact read results - #142 — Remote unbounded atom creation via
order_by - #143 — Documented supervision child spec cannot boot
- #144 —
exposewon't compile on schemas withhas_through - #145 — Batch operations silently no-op
- #146 —
include/preloadablesilently ignored - #147 — Tool results are
inspect/1output, not JSON - #148 —
Ectomancer.child_spec/2returns a bare list - #149 — Broken pluralization in tool names
- #150 —
limitsilently hard-capped at 100 - #151 — README
authorize: with:syntax errors - #152 — Playground + demo assets not shipped in the hex package
- #153 — Batch operations not atomic despite "transactional"/"atomically" claims
1.6.0 - 2026-07-20
Added
MCP Prompts —
prompt/2macro for structured, parameterized prompt templates with argument validation (#122)Define prompts alongside your tools:
prompt :analyze_churn do description "Analyze user churn risk" argument :cohort, :string, required: true, description: "User cohort" messages fn args -> [%{role: :user, content: %{type: :text, text: "Analyze churn for #{args["cohort"]}"}}] end endSupports
:string,:integer,:float,:boolean,:list, and:mapargument types withrequired,description,default, andenumoptions. Arguments are validated by Anubis before the messages callback runs. Registered automatically as MCP prompts — visible viaprompts/listand callable viaprompts/get.Upsert operations —
:upsertaction for insert-or-update workflows with configurable conflict target andon_conflictcontrol (#117)expose MyApp.Products.Product, actions: [:upsert], conflict_target: :sku, on_conflict: :replace_allReturns
{:ok, {record, :inserted | :updated}}. Supports composite conflict targets (conflict_target: [:org_id, :sku]) and selectiveon_conflict: [set: [:name, :avatar_url]]. Automatically restores soft-deleted records.conflict_targetis required at compile time.Batch operations —
batch_create,batch_update,batch_destroyfor transactional multi-record operations (#116)expose MyApp.Accounts.User, actions: [:batch_create, :batch_update, :batch_destroy], batch_size: 200Each runs inside a single
repo.transactionwith individual try/rescue per item — invalid records are collected without aborting valid ones. Returns%{succeeded: [...], failed: [...], total: N}. Configurable:batch_size(default:100) enforced before any DB interaction. Full authorization, scope, and soft-delete support.SSE and WebSocket transport support — three transport options for MCP protocol serving (#120)
Transport Status Route Streamable HTTP (MCP 2025-03-26) Default forward "/", Ectomancer.PlugSSE (MCP 2024-11-05) Deprecated get "/sse"+post "/sse"WebSocket Available socket "/mcp/ws", Ectomancer.Plug.WebSocketStreamable HTTP uses a single endpoint with session header (
mcp-session-id) and streaming responses. WebSocket supports actor extraction via query params orx_headers. UseEctomancer.child_spec/2for multi-transport supervision.Igniter installer —
mix igniter.install ectomancerfor fully automated setup (#119)Automatically: adds dependency, discovers schemas with interactive selection, generates
lib/my_app/mcp.ex, patchesconfig/config.exs, injectsforwardroute into the router, adds a supervisor to the supervision tree via AST patching, and prompts for transport selection. Idempotent — safe to re-run.Per-action authorization for Oban bridge —
expose_oban_jobsnow supports granular authorize rules per action (#121)expose_oban_jobs authorize: [ all: fn actor, _action -> actor.role == :admin end, list_queues: :public, cancel_job: fn actor, _action -> actor.role == :admin end ]Supports function, module,
:none/:public, or keyword lists with:allfallback. Unlisted actions fall through to the global authorization fromuse Ectomancer.Custom MCP Resources —
resource/2macro for defining custom resources alongside auto-generated schema resources (v1.4.0 — listed here for discoverability)Rate limiting — configurable token bucket per tool or globally (v1.4.0)
Changed
- Internal codebase refactored into focused submodules with reduced duplication (#118) — no user-facing API changes
No breaking changes
1.5.0 - 2026-07-17
Added
- Global authorization policy support (#113)
1.4.0 - 2026-07-12
Added
- change: Add :telemetry events for tools, repo, auth, and rate limiter (#110)
Fixed
- Silence installation/teardown logs during test runs (#111)
1.3.1 - 2026-06-24
Added
- Support for Elixir 1.20.0 and OTP 29
Changed
- Updated dependencies to latest compatible versions
Fixed
- Elixir 1.20 compatibility:
- Removed unreachable
parse_auth_handler(nil)clause inEctomancer.Expose - Merged
do_execute/5clauses into single function with runtime arity check inEctomancer.Tool - Eliminated type warnings by conditionally generating authorization and execute code in
Ectomancer.ToolandEctomancer.Resource - Fixed
Authorization.check/3spec to include{:ok, :scoped, fun()}return type - Updated
test/supportloading to useelixirc_pathsinstead ofCode.require_file - Fixed oban bridge test to avoid always-false type assertion
- Removed unreachable
Testing
- Increased test coverage to 80%+
1.3.0 - 2026-05-19
Added
resource/2macro for defining custom MCP resources, parallel to the existingtool/2macro- Static resources (
uri "scheme://path") and templated resources (uri "scheme://{var}") - Optional
authorizeblock for access control (inline function, policy module, or:none) - Configurable
mime_type(defaults to"text/plain") Read handler
fn params, actor -> {:ok, content} | {:error, reason} end
- Static resources (
- Per-schema metadata resources now auto-generated from
exposealongside existing tools :resourceoption inexpose/2acceptsfalseto opt out of per-schema resource generation- 19 new tests for custom resource DSL
Changed
use Ectomancernow importsresource: 2macro- Capabilities updated to include
[:tools, :resources]
1.2.1 - 2026-05-13
Fixed
- Hex publish compatibility — Replaced
inflex(GitHub dep, blocked hex.publish) withplurality(Hex dep) for route name singularization. Plurality is a modern, zero-regex inflection library with verified accuracy across 80k+ noun pairs.
Changed
mix.exsdependency:inflex→plurality ~> 0.2RouteIntrospection.singularize/1now callsPlurality.singularize/1directly
1.2.0 - 2026-05-13
Added
- MCP Resources for schema discovery — Each
exposed schema now automatically registers an MCP resource atectomancer://schemas/{name}returning full schema metadata (fields, types, associations, primary key, available actions). A top-levelectomancer://schemasresource lists all registered schemas. Opt-out per schema withresource: false. (Closes #56) - Dynamic association preloading — New
preloadableoption forexposeallows LLMs to dynamically request associated records via anincludeparameter onlistandgettools. Supportspreloadable: true(all associations) orpreloadable: [:posts, :comments](specific). Requested includes are validated against allowed associations. (Closes #57) - Rate limiting — Token bucket algorithm with ETS storage. Configurable per-tool and global limits. Opt-in via
config :ectomancer, :rate_limits. - Multi-repo support — expose schemas from different repos with
expose User, repo: MyApp.ReplicaRepo. Falls back to global repo config. - Browser MCP client — Zero-dependency HTML browser client at
priv/ectomancer.html. Browse tools, call them, see results. No build step required. - Auto-deployed ExDoc to GitHub Pages — New CI workflow builds docs on push to main and deploys via
actions/deploy-pages. - CI, Hex, and Docs badges to README header.
Fixed
singularizehelper now handles edge cases (status,address,series) correctly.- Error handling now returns structured
{:error, %{code:, message:, details:}}tuples consistently.
Testing
- 426 tests (up from 260), all passing
- 28 new tests: 19 for MCP Resources, 9 for dynamic preloading
- 9 new rate limiter tests
- Validated multi-repo integration with secondary Phoenix app (SQLite)
1.1.0 - 2026-04-23
Added
- Interactive setup tool (
mix ectomancer.setup) for automatic project configuration- Auto-discovers Ecto schemas via module introspection and file scanning
- Prompts for schema selection, Oban bridge, and tool namespace
- Generates MCP module with proper
exposedeclarations - Updates mix.exs, config.exs, and router files automatically
- Derives module name from app name (e.g.,
TestEctoApp.MCP)
- Schema discovery module (
Ectomancer.Installer.SchemaDiscovery) with dual discovery strategy - Config updater (
Ectomancer.Installer.ConfigUpdater) for idempotent file patching - Dependency checker (
Ectomancer.Installer.DependencyChecker) for required/optional dep validation - Template renderer (
Ectomancer.Installer.TemplateRenderer) for MCP module generation - Igniter installer stub (
Ectomancer.Igniter)
Testing
- 260 tests (up from 223), all passing
- Full integration tests for the setup tool workflow
1.0.0 - 2026-03-29
🎉 Official v1.0.0 Release - Production Ready!
Ectomancer is now officially stable and ready for production use! Three phases of development complete.
Added
Optional Oban Bridge (Issue #15) - Phase 3 Final Feature!
- New
expose_oban_jobs/0andexpose_oban_jobs/1macros for Oban integration - Automatically generates 5 MCP tools for job queue management:
list_oban_queues- List all queues with job statistics (total, executing, available, retryable, discarded)get_queue_depth- Get detailed counts for a specific queuelist_stuck_jobs- Find executing jobs with optional filters (queue, worker, min_age, limit)retry_job- Retry failed or discarded jobs by IDcancel_job- Cancel or delete jobs by ID
- Only activates when Oban is in dependencies (optional dependency support)
- Supports
:namespaceoption for tool naming (e.g.,background_list_oban_queues) - Comprehensive test coverage (13 tests)
# Expose all Oban job management tools
expose_oban_jobs
# With namespace prefix
expose_oban_jobs(namespace: :background)
# Generates: background_list_oban_queues, background_get_queue_depth, etc.Phoenix Route Introspection (Issue #14) - Phase 3 Complete!
- New
expose_routes/1macro to auto-generate MCP tools from Phoenix router - Support for all HTTP methods: GET, POST, PUT, PATCH, DELETE
- Smart tool naming with automatic singularization:
/users→get_users,post_users/users/:id→get_user,put_user,delete_user
- Route filtering options:
:only- Include only specific paths:except- Exclude specific paths:methods- Filter by HTTP methods:namespace- Prefix tool names (e.g.,api_get_users)
- Automatic path parameter mapping to tool parameters
- Direct controller action execution via
Plug.Test.conn - Proper handling of
Plug.Conn.AlreadySentError
# Expose all routes
expose_routes MyAppWeb.Router
# With filtering
expose_routes MyAppWeb.Router,
only: ["/api/users"],
namespace: :api,
methods: ["GET", "POST"]Testing
- 223 tests (up from 193)
- 30 new tests: 13 for Oban bridge, 17 for route introspection
- Full integration tested with sweetcorn Phoenix app including:
- Oban job insertion, retry, and cancellation via MCP tools
- Route tool execution through Phoenix controllers
- All authorization strategies working with new features
Issues Closed
- #15 - Create optional Oban bridge for job queue management
- #14 - Implement Phoenix route introspection for MCP tools
0.1.0-rc.3 - 2026-03-18
Added
Read-Only Mode (Issue #12)
- New
:readonlyoption forexpose/2macro - When
readonly: true, only generates:listand:gettools - Prevents create, update, destroy operations
- Perfect for public read-only access to data
expose MyApp.Blog.Post, readonly: true
# Generates only: list_posts, get_postChangeset Error Mapping (Issue #13)
Enhanced error messages from Ecto changeset validations
Automatic categorization of validation errors:
- presence: Missing required fields
- format: Invalid format (email regex, etc.)
- inclusion: Value not in allowed set
- confirmation: Confirmation doesn't match
- length: String length issues
- comparison: Numeric comparison failures
Improved database error detection:
- unique_violation: "Duplicate value: Record with this value already exists"
- foreign_key_violation: "Invalid reference: Related record does not exist"
- not_null_violation: "Missing required parameter: Field Name"
Schema changeset integration
- Uses schema's
changeset/2function when available - Ensures unique_constraint validations work properly
- Returns structured error responses instead of binary strings
- Uses schema's
Changed
- Updated README.md with read-only mode and error handling documentation
- Enhanced error categorization in
format_error/1
Fixed
- Fixed unique constraint violations to return proper error responses
- Fixed foreign key violations to show descriptive messages
- Fixed changeset validation errors to show field names and messages
Testing
- 193 tests (up from 172)
- 21 new tests: 16 for read-only mode, 6 for error mapping
- Full integration tested with sweetcorn Phoenix app
- All authorization strategies still working
Issues Closed
- #12 - Implement read-only mode for expose macro
- #13 - Map Ecto changeset errors to MCP error responses
0.1.0-rc.2 - 2026-03-17
Added
Authorization System (Phase 2)
- Inline function authorization - Simple auth checks with inline functions
authorize fn actor, action -> actor.role == :admin end - Policy module authorization - Reusable authorization logic via behavior
authorize with: MyApp.Policies.UserPolicy - Public access -
:noneauthorization for public endpointsauthorize :none - Per-schema authorization - Global auth rules for all actions on a schema
- Per-action authorization - Fine-grained control with action-specific rules
- Authorization cascade - Multiple auth levels work together
Binary ID / UUID Support
- Full support for
binary_idprimary keys - Automatic UUID string casting
- Works with all CRUD operations
Enhanced Error Messages
- Descriptive error messages (e.g., "Missing required parameter: User id")
- Proper MCP error codes (-32602 for validation, -32603 for internal)
- Field identification in error responses
Changed
- Updated README.md with comprehensive authorization documentation
- Improved error handling with better error categorization
Fixed
- Fixed binary_id primary key handling in get/update/destroy operations
- Fixed Peri validation compatibility with JSON Schema format
- Fixed tool parameter generation for nested blocks
- Fixed atom vs string key handling in normalize_params
Security
- SQL injection prevention via parameterized queries
- Row limits to prevent memory exhaustion (100 records default)
- Authorization checks before tool execution
- Proper error messages without exposing internal details
Testing
- 172 tests (up from 128)
- 35 authorization-specific tests
- All authorization strategies tested
- Full integration tested with sweetcorn Phoenix app
Issues Closed
- #10 - Design and implement authorization hook system
- #11 - Add per-schema and per-action authorization granularity
- #35 - Fix critical bugs in binary ID handling and tool parameter schemas
0.1.0-rc.1 - 2026-03-16
Added
- First release candidate with fully functional CRUD operations
- Core MCP server implementation via
Ectomancermodule expose/2macro for auto-generating CRUD tools from Ecto schemas (list, get, create, update, destroy)tool/2macro for custom tool definitions with param validationEctomancer.Plugfor seamless Phoenix router integrationEctomancer.Repoabstraction supporting all major CRUD operations- Automatic actor extraction and threading through
conn.assigns - Field filtering support via
:onlyand:exceptoptions - Namespace support to prevent tool naming collisions
- Comprehensive test suite (128 tests, all passing)
- Full Credo and Dialyzer compliance
- Support for Phoenix 1.7 and 1.8
- MIT License
Fixed
- Fixed Peri schema validation crashes by disabling params in exposed tools
- Fixed GenServer crashes during CRUD operations with proper error handling
- Fixed tool execution to return proper Anubis Response format
- Fixed repo error handling with comprehensive try/rescue blocks
Security
- SQL injection prevention via parameterized queries in Repo operations
- Row limits to prevent memory exhaustion (100 records default)
- Proper error messages without exposing internal details