Let Claude operate Blazer — explore data sources, write and run SQL, and build dashboards
blazer-mcp adds an authenticated Model Context Protocol (MCP) endpoint to a Rails app running Blazer. Claude (Claude Code, Claude Desktop, claude.ai) connects to it and does anything a person can do in the Blazer web UI — every tool maps 1:1 to a Blazer UI action and runs through Blazer's own API, so an MCP action and the equivalent click are identical, audit rows included.
Add this line to your application's Gemfile:
gem "blazer-mcp"Run:
rails generate blazer:mcp:installThis creates config/initializers/blazer_mcp.rb. Then mount the engine in config/routes.rb:
mount Blazer::Mcp::Engine, at: "/mcp/blazer"The endpoint will not work until you configure authentication.
Claude gets twelve tools, each mapping to a Blazer UI action.
Read:
list_data_sources— list the data sources available to queryget_schema— tables and columns for a data sourcelist_queries— list saved queries (optional search)get_query— a saved query's SQL and variablesrun_query— run a saved query or draft SQL and return rowslist_dashboards— list dashboardsget_dashboard— a dashboard and its ordered querieslist_checks— list checks (alerts)
Write:
create_query— save a new queryupdate_query— update a querycreate_dashboard— create a dashboard from a list of queriesupdate_dashboard— rename a dashboard or replace its queries
Set read_only to drop the write tools.
The gem ships no OAuth server and no role model. Authentication and authorization are two callables you supply — the host app owns identity and policy. Set them in config/initializers/blazer_mcp.rb.
authenticate turns a request into a user, or nil (which returns a 401):
Blazer::Mcp.authenticate = ->(request) { ... return a user or nil ... }authorize decides whether that user may use the endpoint (a falsey return returns a 403). It defaults to allowing any authenticated user:
Blazer::Mcp.authorize = ->(user) { user.admin? }Three recipes follow. None ship as gem code — they touch host models.
If your app is already an OAuth Authorization Server via Doorkeeper, reuse its opaque-token lookup:
Blazer::Mcp.authenticate = ->(request) do
token = request.authorization.to_s.delete_prefix("Bearer ")
access_token = Doorkeeper::AccessToken.by_token(token)
next nil unless access_token&.accessible?
User.find_by(id: access_token.resource_owner_id)
endIf a managed provider (Auth0, WorkOS, Stytch) issues JWT access tokens, verify the signature against its JWKS endpoint:
Blazer::Mcp.authenticate = ->(request) do
token = request.authorization.to_s.delete_prefix("Bearer ")
next nil if token.empty?
payload, = JWT.decode(token, nil, true, algorithms: ["RS256"], jwks: JWKS_LOADER)
User.find_by(external_id: payload["sub"])
rescue JWT::DecodeError
nil
endFor local development and CI, the gem ships a generic static-token helper:
Blazer::Mcp.authenticate = Blazer::Mcp::Auth.static_token(ENV.fetch("BLAZER_MCP_TOKEN"))Do not use this in production. The claude.ai and Claude Desktop connector UIs cannot send a static token — only OAuth credentials — so a production server must sit behind OAuth.
The 401 response carries a WWW-Authenticate header advertising resource_metadata_url so MCP clients can discover your Authorization Server. The gem does not serve .well-known documents — your host app does.
run_query runs whatever SQL it is given, exactly like the Blazer query editor's Run button. The gem adds no SQL filtering.
Use a read-only database user for your Blazer data sources, as Blazer recommends. This is the real guardrail against destructive queries.
max_rows is a presentation cap on the MCP payload, not a query-cost guard — the data source timeout and a read-only DB user are the real protection against expensive queries.
Set read_only to remove the four write tools entirely:
Blazer::Mcp.read_only = trueQuery results can contain PII. Serve the endpoint over TLS and rotate tokens.
# Callable -> (request) { user_or_nil }. Required, no default.
Blazer::Mcp.authenticate = ->(request) { ... }
# Callable -> (user) { bool }. Default: allow any authenticated user.
Blazer::Mcp.authorize = ->(user) { user.admin? }
# OAuth protected-resource metadata URL, advertised on a 401. Default: nil.
Blazer::Mcp.resource_metadata_url = "https://api.example.com/.well-known/oauth-protected-resource"
# Remove the four write tools. Default: false.
Blazer::Mcp.read_only = false
# Maximum rows returned by run_query. Default: 500.
Blazer::Mcp.max_rows = 500
# Allowlist of Blazer data source ids. Default: nil (all).
Blazer::Mcp.data_sources = ["main", "replica"]
# Base URL of your mounted Blazer UI — write tools return deep links. Default: nil.
Blazer::Mcp.blazer_url = "https://app.example.com/blazer"
# MCP server identity. Defaults are usually fine.
Blazer::Mcp.server_name = "blazer-mcp"
Blazer::Mcp.server_version = Blazer::Mcp::VERSIONFor Claude Code, add the endpoint to .mcp.json:
{
"mcpServers": {
"blazer": {
"type": "http",
"url": "https://app.example.com/mcp/blazer"
}
}
}For Claude Desktop and claude.ai, add it as a custom connector using the same URL. The connector flow uses OAuth, so the host app must run an OAuth Authorization Server.
- Ruby 3.3+
- Blazer 3.x
- Rails 7.2+
Tested against Blazer 3.2, 3.3, and 3.4, and against mcp 0.8 and 0.17. The mcp gem is pre-1.0 and has shipped breaking changes in minor releases, so only those two versions are proven. A host running multiple MCP servers should pin mcp to a single consistent version across them.
The gem is available as open source under the terms of the MIT License.