Contributing
Thanks for contributing! Here's how to get started:
- Open an issue to discuss the proposed change
- Create a branch from
main - Implement the change with tests
- Make sure
bundle exec rspecandbundle exec rubocoppass - Open a pull request targeting
main
Setup
git clone git@github.com:fluence-eu/actionmodelcontext.git
cd actionmodelcontext
bundle install
Requires Ruby 3.3+. The gem supports Rails 7.2+ and mcp ~> 1.8.
Development
Running tests and linting
bundle exec rspec # specs, against Rails 8.1 by default
bundle exec rubocop # rubocop-rails-omakase, with single-quoted strings
bundle exec rubocop -a # auto-fix linting
SimpleCov fails the run below 95 % of lines overall or 70 % in a file, so running a single spec file exits with status 2 even when its examples pass.
bundle exec guardian-ci docs --report measures the YARD documentation with yardstick, which fails below 95 %, and bundle exec guardian-ci docs --build generates the site in doc/.
The specs run against a dummy Rails app in spec/dummy. CI runs them on every Rails version the gem supports, selected by RAILS_VERSION. Gemfile.lock is not committed, so re-resolve the bundle with the same variable to run them locally against another version, and run bundle update without it to come back to the default:
RAILS_VERSION='~> 7.2.0' bundle update
RAILS_VERSION='~> 7.2.0' bundle exec rspec
Commit Convention
This project follows Conventional Commits.
Format
<type>(<scope>): <description>
[body]
[footer(s)]
Types
| Type | Description |
|---|---|
feat |
New feature |
fix |
Bug fix |
docs |
Documentation only changes |
style |
Formatting changes (whitespace, commas, etc.) — no logic |
refactor |
Code change that neither fixes a bug nor adds a feature |
perf |
Performance improvement |
test |
Adding or updating tests |
build |
Changes to build system or dependencies |
ci |
CI/CD configuration changes |
chore |
Other changes that don't modify source or tests |
revert |
Revert a previous commit |
Scope
Optional — indicates the area of the gem affected:
| Scope | Area |
|---|---|
tool |
ActionModelContext::Tool and its arguments |
resource |
ActionModelContext::Resource |
prompt |
ActionModelContext::Prompt |
server |
The controller concern, ActionModelContext::Server |
oauth |
config.action_model_context.oauth, the metadata document and the 401 challenges |
i18n |
Translation lookups |
generators |
The install, tool, resource and prompt generators |
test-helper |
ActionModelContext::TestHelper |
railtie |
Rails integration: autoloading of app/mcp, configuration, initializers |
Description
- Imperative mood ("add", not "adds" or "added")
- No capital letter at the start
- No period at the end
- Maximum 72 characters
Body (optional)
- Use bullet points with
-for multiple items - Explain why, not what (the diff speaks for itself)
Breaking Changes
Non-backward-compatible changes must be flagged with:
- A
!after the type/scope:feat(tool)!: rename the required option - And/or a
BREAKING CHANGE:footer in the body
Examples
feat(tool): add arguments_from
fix(oauth): build the challenge's metadata URL from resource
- a proxy that strips the path prefix leaves SCRIPT_NAME empty
- the public URL declared in resource still holds the prefix
refactor(tool): store an argument as one flat hash
feat(tool)!: rename the required option
BREAKING CHANGE: argument takes mandatory: instead of required:,
tools must update their declarations.
Branching Model
main— default branch. All feature branches are created from here, and all PRs target this branch.- Never push directly to
main— always create a feature branch and open a PR.
Pull Request Convention
PR Title
- Same format as commits:
<type>(<scope>): <description> - Under 72 characters
PR Classification
| Level | Criteria | Description requirements |
|---|---|---|
| Critical | Breaking change, security fix | Full description — summary, changes, impact, rollback plan, how to test |
| Major | New feature, significant refactor | Summary + changes + how to test |
| Minor | Small bug fix, config tweak, dependency bump | Summary + changes (1-2 lines each) |
| Trivial | Documentation, formatting, comment update | One-line summary is enough |
PR Description by Level
Critical / Major
## Summary
<What and why in 2-3 sentences>
## Changes
- <change 1>
- <change 2>
## How to Test
1. <step 1>
2. <step 2>
For Critical PRs, also add:
## Impact
- <what parts of the system are affected>
## Rollback Plan
- <how to safely revert if something goes wrong>
Minor
## Summary
<One sentence>
## Changes
- <change 1>
Trivial
A one-line summary in the PR body is sufficient.
Rules
- One topic per PR — don't mix unrelated changes
- All tests and RuboCop must pass
- New code must include tests
- Never commit secrets or credentials
Versioning & Releases
Versions are dated, not semantic: a release is versioned by its UTC timestamp, such as 2026.10.28.143000, and tagged v2026.10.28.143000.
- Write the changelog. Before a release, the
## [Unreleased]section ofCHANGELOG.mdis written by hand, from the merged PRs, and merged through a normal PR (docs: write the changelog of the next release). - Dispatch
prepare-release.yml. It takes no input: it writes the timestamp intolib/action_model_context/version.rb, turns## [Unreleased]into## [<version>] - <date>, and opens an auto-merged release PR. It fails if## [Unreleased]is missing or empty. - Merging the release PR triggers
release.yml(it fires on any push touchingversion.rb), which creates the tag and the GitHub Release with that changelog section as notes, thenpublish.ymlpushes the gem to GitHub Packages andpublish-rubygems.ymlto rubygems.org.
What this means for a normal PR:
- ❌ Do not edit
version.rb. - ❌ Do not edit
CHANGELOG.mdin a feature PR — the changelog is written for the release, not per PR. - ❌ Do not add a "release: vX" or "bump version" commit.
Architecture
lib/
action_model_context.rb # entry point
actionmodelcontext.rb # Bundler require shim
action_model_context/
primitive.rb # shared by tools, resources and prompts: naming, I18n, rescue_from
tool.rb # arguments as JSON Schema 2020-12, call
resource.rb # uri and URI templates, read
prompt.rb # arguments, call
server.rb # controller concern: discovery, transport, OAuth
railtie.rb # autoloading of app/mcp, config.action_model_context
protected_resource_metadata_middleware.rb # the SDK's middleware, served under the app's prefix
translation.rb, naming.rb, error.rb, error_reporter.rb, test_helper.rb, version.rb
generators/ # install, tool, resource and prompt, with their RSpec and Minitest generators
- The protocol is delegated to the official
mcpSDK; the gem only adds the Rails layer. Reuse the SDK rather than reimplementing it. - Primitives live under
app/mcp/{tools,resources,prompts}, collapsed by Zeitwerk, and are discovered through thedescendantsofApplicationTool,ApplicationResourceandApplicationPrompt. - The server and the transport are built for each request, stateless.
Tests
- Framework: RSpec (
spec/), with a dummy Rails app inspec/dummy - Request specs go through a real
MCP::Serverand transport (spec/requests) - All new code must be tested; a mechanism that shares state between requests needs a spec with real threads, like
spec/requests/mcp_concurrency_spec.rb
Code Conventions
- Strings: single quotes, unless interpolation or escapes require double quotes
- Block parameters: named (
{ |tool| tool.name }), never numbered (_1,_2…) nor the implicitit - One-expression methods are endless:
def argument_names = arguments.keys - Hash and keyword shorthand:
argument(name, type, null:), notnull: null - Comments only for the non-obvious why, and generic: no deployment-specific names, hosts or paths
- YARD: a summary and
@returnon every method,@api publicfor what the README documents and@api privatefor the rest, which stays off the site - RuboCop: no rule disabling without justification
CI
GitHub Actions:
- ci.yml — on every PR and on
main: RSpec on Ruby 3.3 to 4.0 × Rails 7.2, 8.0 and 8.1 - rubocop-measure.yml, rubocop-pr.yml — RuboCop offense baseline + sticky offense-delta comment on PRs, failing on any offense
- coverage-measure.yml, coverage-pr.yml — coverage baseline + undercover regression check
- codequality-measure.yml, codequality-pr.yml — reek/flog/flay advisory deltas
- docs-measure.yml, docs-pr.yml, docs-deploy.yml — yardstick baseline + sticky comment on PRs, failing below 95 %, and the YARD site on GitHub Pages
- prepare-release.yml, release.yml, publish.yml, publish-rubygems.yml — the release pipeline, see Versioning & Releases
- claude-review-pr.yml — Claude's review on every PR except release PRs, advisory: it never fails the PR
- dependabot.yml — weekly update PRs for the gems and the actions
AI-Assisted Contributions
When using AI tools (Claude, Copilot, etc.) to generate commits or PRs:
- Classify first — determine if the change is critical, major, minor, or trivial
- Focus on the "why" — the diff already shows the "what"
- Skip boilerplate — don't add empty sections or placeholder text
The person opening the PR takes full responsibility for the code. Before submitting:
- [ ] I have read and understood every line of code in this PR
- [ ] I can explain why each change was made
- [ ] I have tested the changes locally
Do not add AI co-author lines (Co-Authored-By), "Generated with" footers, or any AI attribution in commits or PR descriptions.
License
By contributing, you agree that your contributions will be licensed under the MIT License.