Skip to content

About

Model Context Protocol (MCP) endpoint served directly from a Grav site over Streamable HTTP

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Model Context Protocol

Grav MCP Server Plugin

Serves a Model Context Protocol endpoint directly from your Grav site over Streamable HTTP. Point your MCP client or LLM/AI agent at https://yoursite.com/mcp.

Works with hosted connectors (built-in OAuth 2.1 authorization server with dynamic client registration) and CLI or desktop clients (OAuth or a static API key header).

How it relates to the existing pieces

  • grav-plugin-api — REST API + API key management. Required: this plugin is a thin MCP translation layer over it — Bearer auth reuses its grav_... API keys, OAuth access tokens are minted through its ApiKeyManager, and tools reuse its services rather than duplicating them.
  • grav-mcp — the local-process MCP server that bridges stdio → Grav REST API, for local clients like Claude Code and Cursor on your own machine. This plugin complements it as the hosted alternative: MCP served from the site itself, for hosted connectors like claude.ai and ChatGPT where a local process isn't an option. The tool surface tracks the API plugin directly (see DECISIONS.md).

Requirements

  • Grav 2.1.5+ (what API plugin 1.0.44 requires)
  • PHP 8.3+ (Grav 2 core's own floor)
  • API plugin 1.0.44 or newer, installed and enabled, with at least one API key: bin/plugin api keys:generate --user=admin --name="MCP"

Tools map 1:1 onto API plugin endpoints, so an older API plugin 404s on tools backed by newer endpoints. GPM enforces the version floor; a git clone doesn't — the plugin then logs a warning at client handshake, and site_info reports api_plugin_version. A Grav 2.0 site can install API plugin 1.0.30 at most, so it stays on mcp-server 1.3.3.

Installation

Install with GPM — the preferred path, since GPM also installs the required API plugin and enforces its version floor (directory listing):

bin/gpm install mcp-server

For development, clone into your site's plugin folder instead; the directory name must be mcp-server:

cd user/plugins
git clone https://github.com/sandymac/grav-plugin-mcp-server mcp-server

Configuration

user/config/plugins/mcp-server.yaml:

enabled: true
route: /mcp
require_auth: true   # never disable on a public site

oauth:
  enabled: true                # required for hosted connectors (interactive sign-in)
  access_token_days: 7
  refresh_token_days: 90
  require_permission: api.access  # permission needed to approve a connection ("API Access" in account permissions)
  allowed_redirect_hosts:      # localhost always allowed; empty = any https host
    - claude.ai
    - claude.com

Your web server must pass unmatched /.well-known/* paths through to Grav's index.php (the standard try_files $uri /index.php?$args nginx setup already does).

Client setup

Hosted connector (OAuth)

In the client's connector settings, add a custom connector with URL https://grav.example.com/mcp. Leave any OAuth client ID/secret fields empty — the client registers itself via dynamic client registration. When prompted, sign in with your Grav credentials on the consent screen and approve.

Registration is refused when any redirect URI the client sends is off oauth.allowed_redirect_hosts. The defaults cover claude.ai, ChatGPT, Gemini (all three oauth-redirect*.googleusercontent.com hosts it registers at once), Mistral, and the VS Code / Cursor / Windsurf sign-in hosts; a client whose registration is rejected leaves a registration rejected line in grav.log naming the offending URI, so a customized list can be fixed from there.

Behind the scenes: the client discovers /.well-known/oauth-protected-resource/mcp → registers at /mcp/oauth/register → authorization-code + PKCE flow at /mcp/oauth/authorize → tokens from /mcp/oauth/token. The access token is a real grav_ API key (visible in bin/plugin api keys:list); revoke it at /mcp/oauth/revoke (revoking either the refresh token or access key revokes both). Deleting the key any other way — from the user's page in Admin2 or with bin/plugin api keys:revoke — ends the connection too: its next refresh is refused.

A client may request a scope of api.* permissions (advertised as scopes_supported in the discovery metadata); the granted scopes cap the minted key and are shown on the consent screen. A request that limits nothing — no scope, *, or the whole advertised list, which is what today's connectors send — mints an unscoped key, and the consent screen says so: effective access is always the account's live permissions intersected with the key's scopes, so a dedicated bot account remains the best way to manage what a connector can do over time.

Every permission on the consent screen is a checkbox. Untick any to exclude it from the grant: the minted key is capped at what stayed ticked, and the token response reports the narrower scope (RFC 6749 §3.3). Some clients never read that echo and assume they hold what they requested — a narrowed connection then sees fewer tools and gets permission errors on the excluded ones, which is the intended effect. The selection is frozen at approval time — a later upgrade that adds permissions does not widen it — so whoami reports the key's scopes and lists hidden tools by cause (hidden_by_key_scope versus hidden_by_account_permission). To change the selection, revoke and reconnect, or manage the key with bin/plugin api keys:*.

CLI or desktop client (OAuth or API key)

Every client needs the same two pieces: the endpoint URL, and — to skip the browser flow — an Authorization: Bearer grav_... header. For example, with Claude Code:

claude mcp add --transport http grav https://grav.example.com/mcp --header "Authorization: Bearer grav_your_key_here"

(omit --header to use the OAuth sign-in instead). Or in an .mcp.json:

{
  "mcpServers": {
    "grav": {
      "type": "http",
      "url": "https://grav.example.com/mcp",
      "headers": {
        "Authorization": "Bearer grav_your_key_here"
      }
    }
  }
}

Plugin tools

Other installed plugins can publish their own REST routes as MCP tools — an mcp.yaml manifest at the plugin root, or the onApiMcpTools event — served by the API plugin's GET /mcp/tools and offered here alongside the built-in tools. If you're authoring a plugin, follow the manifest reference in grav-plugin-api's README ("MCP tool manifests" section): https://github.com/getgrav/grav-plugin-api. A manifest that names a body argument (manifest version 2, as Flex Objects 1.4.13 ships for its flex_* tools) is only served by API plugin 1.0.28 or later; an older API plugin skips the whole manifest with a warning, so those tools do not appear until the API plugin is updated.

Controlled by the plugin_tools config toggle (on by default; set to false to serve only the built-in tools). Like every tool here, visibility follows the caller's own Grav permissions — a plugin tool only shows up for accounts that hold whatever permission it declares.

This is also why the OAuth consent screen mints an unscoped key for a "full account access" grant: a key capped to this plugin's own advertised scopes could never reach a permission a plugin declares on its own. The consent screen explains this and offers a checkbox to freeze the grant to the listed vocabulary instead, for connectors that should be limited to the built-in tools — plugin-published tools, installed now or added later, need permissions outside that list and are excluded by a frozen grant. Unticking any listed permission has the same freezing effect: the grant becomes exactly what stayed ticked.

Verify with curl

curl -s https://grav.example.com/mcp \
  -H "Authorization: Bearer grav_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'

Status

63 tools across 12 domains (pages, multilingual, translations, media, config, users, GPM, system, dashboard, webhooks, blueprints, plugins) plus site_info, 5 resources, and 7 prompts, tracking the API plugin's REST surface. Every tool call dispatches in-process through the API plugin's own router, so its permission scopes, page ACLs, ETag conflict handling, audit trail, and rate limiting all apply unchanged. A connected client only sees the tools its key scopes and its account's permissions allow — a limited bot account advertises a correspondingly small tool list. Validated end-to-end on a live deployment as both a claude.ai custom connector and a Claude Code HTTP server.

Development

Protocol-level smoke test (no Grav install needed):

php tests/smoke.php

About

Model Context Protocol (MCP) endpoint served directly from a Grav site over Streamable HTTP

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages