A Grav Plugin That Turns Your Site Into an MCP Server
This site now runs grav-plugin-mcp-server, my first published Grav plugin. It serves a Model Context Protocol endpoint straight from the site itself — point an MCP client at https://yoursite.com/mcp and you can edit pages, manage media, adjust config, and audit users using whatever AI tool you already use.
This post was written and published through it.
The problem it solves
The existing option, grav-mcp, is a local-process server that bridges stdio to Grav's REST API. That works, but it means installing and configuring a separate process on every machine you want to edit from — and it rules out hosted connectors entirely, since a service like claude.ai can't reach a program running on your laptop.
Serving MCP from the site removes that layer. The endpoint is just another route. Any client that speaks Streamable HTTP can reach it, from anywhere, with no local install.
A translation layer, not a reimplementation
The plugin is deliberately thin. It sits on top of grav-plugin-api and every tool call dispatches in-process through that plugin's own router. Nothing is reimplemented.
The practical consequence is that the API plugin's permission scopes, page ACLs, ETag conflict handling, audit trail, and rate limiting all apply unchanged. There is no second security model to reason about or keep in sync — a bug class I'd rather not introduce into something that accepts remote write requests.
It also means the tool list is per-connection. A client only sees the tools its API key's scopes and its account's permissions allow. Give a bot account narrow permissions and it advertises a correspondingly narrow tool list; the tools it can't use simply don't appear.
Current surface: 49 tools across 11 domains — pages, multilingual, media, config, users, GPM, system, dashboard, webhooks, blueprints, and plugins — plus site_info, 5 resources, and 6 prompts.
Two ways to connect
Hosted connectors use a built-in OAuth 2.1 authorization server with dynamic client registration. Add a custom connector pointing at your /mcp URL and leave the client ID and secret fields empty — the client registers itself, then walks an authorization-code + PKCE flow. You sign in with your Grav credentials on a consent screen and approve.
The token you get back is a real grav_ API key, visible in bin/plugin api keys:list like any other. Revoking either the refresh token or the access key revokes both.
CLI and desktop clients can do the same OAuth flow, or skip the browser with a static Authorization: Bearer grav_... header:
claude mcp add --transport http grav https://grav.example.com/mcp \
--header "Authorization: Bearer grav_your_key_here"
Requirements
- Grav 2.0.17 or newer
- PHP 8.3+ (Grav 2 core's own floor)
- API plugin 1.0.19 or newer, enabled, with at least one API key
That version floor matters. Tools map one-to-one onto API plugin endpoints, so an older API plugin will 404 on anything backed by a newer endpoint. GPM enforces the floor; a git clone doesn't, so the plugin logs a warning at client handshake and reports api_plugin_version through site_info.
One web server note: unmatched /.well-known/* paths need to reach Grav's index.php for OAuth discovery to work. The standard nginx try_files $uri /index.php?$args setup already does this.
Installation
The directory name has to be mcp-server:
cd user/plugins
git clone https://github.com/sandymac/grav-plugin-mcp-server mcp-server
Then in user/config/plugins/mcp-server.yaml:
enabled: true
route: /mcp
require_auth: true # never disable on a public site
oauth:
enabled: true
access_token_days: 7
refresh_token_days: 90
require_permission: api.access
allowed_redirect_hosts:
- claude.ai
- claude.com
require_auth: true is not a default worth changing on anything reachable from the internet.
Status
Validated end to end on a live deployment as both a claude.ai custom connector and a Claude Code HTTP server. MIT licensed. Issues and pull requests welcome at github.com/sandymac/grav-plugin-mcp-server.