Skip to content
How-to guides and worked examplesSee the guides →

MCP Server reference ​

Per-item reference for the avo-mcp_server add-on. For installation, connecting a client, and worked examples, see the MCP Server guide.

All options live under config.mcp_server inside Avo.configure, in config/initializers/avo.rb:

ruby
Avo.configure do |config|
  config.mcp_server.enabled = true
  config.mcp_server.resource_identifier = "https://app.example.com/avo/mcp"
  config.mcp_server.tool_calls_per_minute = 300
  config.mcp_server.connection_log = true
  config.mcp_server.connection_log_size = 500
end

Configuration ​

enabled

Turns the server on. While false, every protocol endpoint and the consent screen answer 404 and no new connection can be authorized. The MCP connections resource belongs to the panel rather than to the server, so it stays available — existing connections can be reviewed and revoked while the server is off.

ruby
config.mcp_server.enabled = true
  • Type: Boolean
  • Default: false

resource_identifier

The public URL of the MCP server — the string an admin pastes into their AI client. Left unset, the address is derived per request from the origin the request arrived on plus the mount path. Once set, it is the single value read by the discovery document's resource field, the audience stamped on every issued token, and the audience checked on every incoming call; a request arriving at any other scheme, host, or port is answered 421 Misdirected Request.

ruby
config.mcp_server.resource_identifier = "https://app.example.com/avo/mcp"
  • Type: String
  • Default: nil — derived from the request origin plus the mount path
  • Validation: raises Avo::McpServer::ConfigurationError at boot when its path differs from the path passed to mount_avo_mcp_server, or when it uses http:// on a host other than localhost or 127.0.0.1.

Changing it is a migration

Tokens are bound to the address they were minted under. After a change, every live connection's next call is answered 421, and admins reconnect through the authorize page.

tool_calls_per_minute

How many JSON-RPC requests one connection may make in a minute before the endpoint answers 429 with a Retry-After header. The counter runs through Rails.cache: on :null_store nothing is counted, and on a per-process store each worker keeps its own count.

ruby
config.mcp_server.tool_calls_per_minute = 600
  • Type: Integer
  • Default: 300
  • Values: nil turns the ceiling off

connection_log

Whether the JSON-RPC endpoint records every request a connection makes — tool calls, list requests, the handshake, and the ones it refused — for the Log card on the connection's page. Rows are written after the endpoint has rendered its answer and never fail a request.

ruby
config.mcp_server.connection_log = false
  • Type: Boolean
  • Default: true

connection_log_size

How many log rows each connection keeps. Older rows are dropped as new ones arrive.

ruby
config.mcp_server.connection_log_size = 2_000
  • Type: Integer
  • Default: 500
  • Values: nil keeps every row

extra_tools

Tools of the app's own, served beside the ten. Each entry is a class name — never a constant, since the initializer runs before the app's classes are loadable — or a Hash whose tool: key names the class. The class is either an Avo::McpServer::Tool, which the avo:mcp_server:tool generator scaffolds, or a RubyLLM::Tool written for Avo AI that declares def self.capability; the second is served through an adapter. Either shape names the resources it touches (resources "Ticket", "Project", or def self.resources on a RubyLLM::Tool), which a connection narrowed to named resources must all grant; a call that names none is refused on such a connection. Names are resolved on every request, and an entry that can't be served is dropped with a log line naming the reason while the rest keep serving. See Add tools of your own.

A Hash entry's remaining keys are passed to the tool's initializer, so the Hash form is for a RubyLLM::Tool. An Avo::McpServer::Tool runs as class methods and is never instantiated; an entry naming one and carrying settings is dropped rather than served without them.

ruby
config.mcp_server.extra_tools = [
  "IssueInvoiceTool",                            # an Avo::McpServer::Tool takes no settings
  {tool: "CrmTool", api_key: ENV["CRM_API_KEY"]} # a RubyLLM::Tool receives them in its initializer
]

user: is reserved: the key is dropped from an entry, and the admin the connection was authorized by is passed instead to a tool whose initializer accepts one.

Entry dropped becauseApplies to
The class name doesn't resolveboth
The class is neither an Avo::McpServer::Tool nor a RubyLLM::Toolboth
It declares no capability, or one that isn't avo:read, avo:write or avo:actionsboth
Its tool name is already taken by a shipped tool or by an earlier entryboth
The entry carries keys besides tool:Avo::McpServer::Tool
It overrides call, where the capability gate runsAvo::McpServer::Tool
Reading its capability, name, description, resources or argument schema raisedRubyLLM::Tool
Its argument schema doesn't resolve to an objectRubyLLM::Tool
  • Type: Array of String or Hash with a String tool: key
  • Default: []
  • Validation: raises ArgumentError at boot for an entry that is neither a String nor a Hash with a String tool: key. Everything else is checked per request, and a failure drops that entry alone.

Routing ​

mount_avo_mcp_server

The routing method that draws every endpoint an AI client calls. Available inside Rails.application.routes.draw.

ruby
# config/routes.rb
mount_avo_mcp_server                    # JSON-RPC endpoint at /avo/mcp
mount_avo_mcp_server at: "/admin/mcp"   # moved
EndpointPathAuthentication
Protected resource metadata/.well-known/oauth-protected-resource (origin root)None
Authorization server metadata/.well-known/oauth-authorization-server (origin root)None
Token endpoint<mount path>/tokenNone
Client registration (RFC 7591)<mount path>/registerNone
Token revocation (RFC 7009)<mount path>/revokeNone
JSON-RPC endpoint<mount path>Bearer token

The discovery documents are always at the origin root, where the protocol requires them; at: moves only the endpoints under the mount path. The consent screen and the MCP connections resource are mounted with the panel, not here.

  • Type: routing method
  • Values: at: — the JSON-RPC endpoint's path. Default "/avo/mcp".
  • Validation: raises Avo::McpServer::ConfigurationError at boot when drawn inside an authenticate or constraints block, when drawn after mount_avo at a path under Avo's mount prefix, or when at: differs from the path in a pinned resource_identifier.

Generators ​

avo:mcp_server install

Writes the migration and appends the configuration block to config/initializers/avo.rb.

bash
bin/rails generate avo:mcp_server install
bin/rails db:migrate
MigrationTables
create_avo_mcp_serveravo_mcp_server_connections, avo_mcp_server_access_grants (single-use codes), avo_mcp_server_access_tokens (digests), avo_mcp_server_clients (registered clients), avo_mcp_server_events (the connection log)
add_slug_to_avo_mcp_server_connectionsOnly for an app whose create_avo_mcp_server predates the slug column on avo_mcp_server_connections — the connection's name. Names every existing connection. A fresh install has the column in the first migration.

The installer is additive. A migration the app already has is skipped, and the configuration block is appended only when config.mcp_server.enabled appears nowhere in the initializer. Run it again after upgrading the gem: it reads the app's own create_avo_mcp_server migration and adds the upgrade migration only when that one lacks the columns.

  • Values: --skip-avo-version — don't print the installed Avo version

avo:mcp_server:tool

Scaffolds a tool of the app's own under app/tools/: an Avo::McpServer::Tool with the wire name pinned, a capability, a description, an input schema and a perform skeleton. Register the class with extra_tools.

bash
bin/rails generate avo:mcp_server:tool issue_invoice

Writes app/tools/issue_invoice_tool.rb, defining IssueInvoiceTool, called issue_invoice by a client. issue_invoice, issue_invoice_tool and IssueInvoiceTool name the same tool; the suffix is added once. An app that also runs Avo AI scaffolds with avo:ai:tool instead and registers the one class with both gems.

Tools ​

The ten tools a connection can call, each gated by the capability it declares. Tools the app registers through extra_tools are served beside them, gated the same way by the capability each declares, and by the resources each declares. Tool names are what a client prints in its transcript and are never translated.

ToolCapabilityDoes
list_resourcesavo:readLists the resources this panel serves
list_recordsavo:readPages through a resource's records
count_recordsavo:readCounts a resource's records without returning any
show_recordavo:readOpens one record in full, with its associations
search_recordsavo:readSearches records by text
list_actionsavo:readLists the actions a resource registers
create_recordavo:writeCreates a record
update_recordavo:writeChanges a record's fields
delete_recordavo:writeDeletes a record
run_actionavo:actionsRuns one of this app's actions on records

Arguments ​

ToolRequiredOptional
list_resources——
list_recordsresourcepage, per_page, sort_by, sort_direction
count_recordsresource—
show_recordresource, id—
search_recordsqueryresource, limit
list_actionsresource—
create_recordresource, attributes—
update_recordresource, id, attributes—
delete_recordresource, id—
run_actionresource, actionrecord_ids, fields
  • resource is a resource name as list_resources reports it ("Post"), never a table name. id and each of record_ids is the record's id as the tools return it: its to_param, the id in the panel's URL (a slug or a hashid where the model hides its primary key), always a string. It is found through the resource's find_record_method, within the admin's policy scope.
  • Paging is 1-based. per_page defaults to 25 and is capped at 100; search_records' limit behaves the same way, per resource searched.
  • sort_by must name a column the resource exposes to the connecting admin, or created_at, which the panel sorts by even when no field declares it; anything else is refused with sortableColumns. sort_direction is asc or desc. Left out, the resource's default sorting applies, falling back to created_at and then the primary key, as the panel does.
  • attributes maps the names the resource's edit form takes to values: a field name (as list_resources reports it) or the param the form submits. A belongs_to is written as user or user_id, a polymorphic one as commentable_type and commentable_id together. The record is filled through the resource's fields (fill_record), so update_using, a belongs_to's finder and attach scope, and a key-value field's locks all apply. A column a field writes under another name (a money field's price_cents) and a field the form doesn't render (a badge) are refused, and so are id, created_at, updated_at, and names matching password, token, secret, or digest. Values are JSON types: an object for a key-value field, {option: true} for a boolean group, ISO 8601 with an offset for a date-time. A refused attribute fails the call with fields naming it. update_record changes only the attributes passed.
  • run_action takes the action id from list_actions (the class name) and fields keyed by the input names it reports. A standalone action takes no record_ids and is refused if given any; every other action needs at least one. Every id is authorized individually, so a partly-allowed batch is refused rather than partly run. An action whose self.visible or self.authorize block hides it from the admin, as the panel would, is left out of list_actions and refused by run_action.
  • count_records returns {resource, count} and nothing else. The count is list_records' total for the same connection: the resource's index_query narrowed by the admin's policy scope, with no default filter or scope from the index screen applied.
  • Every id a tool returns is a to_param: a record's, a belongs_to's, and a has_many's. show_record returns a belongs_to as the record's id and title, a has_one as the record's id and title read through the associated resource's policy scope (null when the scope hides it), and a has_many as a count plus up to 25 ids, narrowed by the associated resource's policy scope. An association the record's policy refuses with view_<association>? is left out, as its panel is on the show page.
  • search_records runs each resource's own self.search block. A resource without one is an error when named directly and is listed under unsearchable in an all-resource search; it never falls back to listing the resource's records.
  • The MCP connections resource is excluded from every tool.

Capabilities ​

The scope strings a grant is stored as. Write carries read; a per-resource grant appends the resource's name.

CapabilityScope stringPer-resource formSelected at consent
Readavo:readavo:read:PostYes
Read & writeavo:writeavo:write:PostNo
Run actionsavo:actions—No

Run actions has no per-resource form: over a narrowed read it applies to the resources the read grant names. A connection's capabilities are set at consent and changed later from the connection's Entitlements card, by whoever the policy's edit_entitlements? allows; the change binds the connection's next tool call on the tokens the client already holds.

Error codes ​

Refusals are JSON-RPC errors carrying a numeric code, a message, and a data object.

CodeConstantMeansdata carries
-32000CAPABILITY_REQUIREDThe connection was never granted the capability the tool requiresrequiredCapability, grantedCapabilities
-32001UNAUTHORIZEDCapability held; the owning admin's policy said noaction, subject
-32002AUTHORIZATION_UNAVAILABLEAuthorization isn't licensed, so nothing was attempted—
-32003VALIDATION_FAILEDThe model refused a create, update, or destroyvalidationErrors, fieldErrors, requiredAttributes, optionalAttributes, missingRequiredAttributes
-32004SEARCH_UNAVAILABLEThe resource has no self.search configuredresource
-32005UNSUPPORTED_RESOURCEThe resource is not backed by Active Recordresource
-32602INVALID_PARAMSUnknown tool, resource, record, or argumentwritableFields, availableActions, availableInputs, or sortableColumns, depending on the argument
-32020—An Mcp-Method, Mcp-Name, or Mcp-Protocol-Version header disagrees with the body—
-32022—A protocol revision this server doesn't implement—

The first six are the add-on's own, in the -32000 to -32019 band the MCP specification leaves to implementations; the rest are the specification's.

-32000 names what was withheld and what the connection holds, so a client can tell the admin what to re-authorize:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32000,
    "message": "This connection was not granted avo:write, which is required by delete_record.",
    "data": {
      "requiredCapability": "avo:write",
      "grantedCapabilities": ["avo:read"]
    }
  }
}

-32003 is shaped for an agent that should correct its own call. requiredAttributes is read from the schema and the model — NOT NULL columns without a default, presence validators, non-optional belongs_to — which is what makes missingRequiredAttributes worth acting on:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32003,
    "message": "Post could not be saved: Title can't be blank",
    "data": {
      "validationErrors": ["Title can't be blank"],
      "fieldErrors": { "title": ["can't be blank"] },
      "requiredAttributes": ["title", "user_id"],
      "optionalAttributes": ["body", "published_at"],
      "missingRequiredAttributes": ["title"]
    }
  }
}

A record outside the admin's policy scope reports as -32602, identical to an id that never existed. A resource they may not list reports as -32001. A successful result carries "resultType": "complete".

Connection log ​

One row per request a connection makes, shown on the connection's page as the Log card and polled from <your-avo-path>/mcp_server/connections/:id/events. Rows are written by the JSON-RPC endpoint after it has answered, and pruned per connection to connection_log_size.

OutcomeRecorded when
okThe request was served
errorThe answer was a JSON-RPC error; the row keeps its code and message
tool_errorThe tool's result carried isError; the row keeps its text
refusedThe request was refused for a malformed header before any tool ran
rate_limitedThe connection was over tool_calls_per_minute
rejectedThe token no longer authenticates — expired, revoked, or on a revoked connection

A tool call's arguments are stored after the app's filter_parameters and capped at 4 KB; over that, only the keys and the size are kept. Requests that resolve to no connection — server/discover, or a token that names nothing — are not recorded.

Panel classes ​

ClassWhat it is
Avo::Resources::McpConnectionThe MCP connections resource, at <your-avo-path>/resources/mcp_connections
Avo::Actions::McpRevokeConnectionThe Revoke action
Avo::McpServer::ConnectionThe model behind the resource. Its identity — client, owner, name — is immutable and it refuses destroy; the grant (capabilities) is edited through the Entitlements card.
Avo::McpServer::EventOne row of the connection log
Avo::McpServer::ConnectionPolicyThe policy you write; the gem ships none

Policy methods ​

MethodDecides
use?Required. Whether the admin gets the server at all: every call, code exchange and refresh, the consent screen, the connect page and its buttons. Never revocation or the list. A policy without it refuses every admin. See Turn MCP on or off per admin
Scope#resolveWhich connections are listed and findable
index?Whether the resource appears at all under explicit_authorization
show?A connection's page — and every card on it that the three methods below do not claim
act_on?The Revoke action. Asked once with the class, then per selected connection
view_log?Optional. The Log card and its poll endpoint, in place of show?
view_audit_trail?Optional. The Audit trail table and the endpoint that fills it, in place of show?
view_entitlements?Optional. The Entitlements card, in place of show?
view_tools?Optional. The Tools card, in place of show?
edit_entitlements?Optional. Changing the grant from the Entitlements card, and the request Apply sends. Denied — or undefined in a policy — the card renders read-only; it does not fall back to show?. Editing implies view_entitlements?
create?, edit?, destroy?The corresponding controls. Identity is immutable and the model refuses deletes regardless; the grant is changed through edit_entitlements?, not edit?

The three card methods are separate because the cards disclose different things: the Log carries data about your own records, the Entitlements matrix is computed against the connection owner's reach (so on another admin's connection it names resources the reader's own policies may hide from them), and the Tools list is derived from the registry and the grant. Refusing one leaves the rest of the page intact.

A host with an explicit config.resources array must add "Avo::Resources::McpConnection" to it. Avo::Resources::McpConnection.visible_on_sidebar = false in a to_prepare block keeps it off the sidebar.

Translations ​

Every string the resource shows is translatable, in each of the nineteen languages Avo ships: field names under avo.resource_translations.mcp_connection.fields.<field id>, everything else under avo.mcp_server.connections — the Revoke action's copy, the Tools card (.tools.*), the Entitlements card (.entitlements.*), the Log card (.log.*), and the capability titles (.capabilities.read|write|actions). Tool names and descriptions are never translated, and the consent screen is English throughout.

Rate limits ​

EndpointCeilingKeyed by
JSON-RPCtool_calls_per_minute, 300 by defaultConnection
Token60 a minuteIP
Authorize (consent screen)30 a minuteIP
Client registration10 a minuteIP

Over a limit the endpoint answers 429 with a Retry-After header. Every counter runs through Rails.cache.

Protocol ​

ItemValue
MCP revision2026-07-28 (the stateless lifecycle and server/discover), and the classic initialize lifecycle for clients on 2025-11-25 and earlier
TransportStreamable HTTP, JSON responses. No stdio, no event stream, no shared token
Client authenticationPublic clients only (token_endpoint_auth_methods_supported: ["none"]), PKCE required
Client identityClient ID Metadata Document, or RFC 7591 dynamic registration
TokensAudience-bound to resource_identifier or the derived address; access tokens live an hour, refresh tokens rotate with family-wide revocation on reuse
Log filteringcode, code_verifier, refresh_token, access_token, and client_secret are filtered from request logs. _meta is not