Skip to main content
Glama
README.md
# Kirby MCP

[![Kirby 5](https://flat.badgen.net/badge/Kirby/5?color=ECC748)](https://getkirby.com)
![PHP 8.2](https://flat.badgen.net/badge/PHP/8.2?color=4E5B93&icon=php&label)
![Release](https://flat.badgen.net/packagist/v/bnomei/kirby-mcp?color=ae81ff&icon=github&label)
![Downloads](https://flat.badgen.net/packagist/dt/bnomei/kirby-mcp?color=272822&icon=github&label)
![Unittests](https://github.com/bnomei/kirby-mcp/actions/workflows/pest-tests.yml/badge.svg)
![PHPStan](https://github.com/bnomei/kirby-mcp/actions/workflows/phpstan.yml/badge.svg)
[![Discord](https://flat.badgen.net/badge/discord/bnomei?color=7289da&icon=discord&label)](https://discordapp.com/users/bnomei)
[![Buymecoffee](https://flat.badgen.net/badge/icon/donate?icon=buymeacoffee&color=FF813F&label)](https://www.buymeacoffee.com/bnomei)

CLI-first MCP server for Composer-based Kirby CMS projects. It lets an IDE or agent inspect your Kirby project (blueprints, templates, plugins, docs) and interact with a real Kirby runtime. It ships with a local knowledge base of Kirby concepts and tasks. For agent-specific install steps (Claude Code, Codex CLI) and Skill sync, see **Client setup**.

It can also run as a projectless global reference MCP (`kirby-mcp --global`) for always-on Kirby docs/KB research. Global reference mode is intentionally separate from project-local MCP servers and cannot inspect, render, update, or run commands in a Kirby project.

The server uses MCP SDK v0.8 dual-era dispatch: existing clients negotiate stateful sessions through `initialize`, while `2026-07-28` clients use stateless requests. MCP logging requests are not advertised—diagnostics are written to stderr instead. A valid W3C v00 `traceparent` supplied by a modern request, including the native HTTP header from browser clients, is included for correlation; `tracestate` and `baggage` are never logged.

> [!WARNING]
> Prompt injection is a serious security threat, especially when used with documents retrieved from the internet. You might not see it happen when observing the conversation with the agent!

## Quickstart

From your Kirby project root:

```bash
composer require bnomei/kirby-mcp --dev
vendor/bin/kirby-mcp install
vendor/bin/kirby-mcp
```

This quickstart is for a local stdio MCP server. If you want Kirby to serve a production HTTP
`/mcp` route, install `bnomei/kirby-mcp` as a normal Composer dependency instead of `--dev`; see
**HTTP transport** below.

Then configure your MCP client (Cursor/Claude Code/Codex CLI) using the examples in **Client setup** and copy the bundled Skills as described below.

See **Client setup → Claude Code** and **Client setup → Codex CLI** for per-agent install and Skill sync steps.

## Global Reference Mode

Install once with Composer and run the global reference MCP anywhere:

```bash
composer global require bnomei/kirby-mcp
kirby-mcp --global
```

Use this mode for always-available Kirby research: bundled KB search, glossary, Panel field/section reference, hooks, extensions, update-schema guides, official docs search, and plugin directory search.

Global reference mode is projectless by design:

- It does not auto-detect or accept `--project`.
- It does not expose project/runtime tools such as `kirby_roots`, `kirby_info`, `kirby_render_page`, content updates, eval/query, runtime install, or IDE helper generation.
- If you need project context or mutations, add a separate project-local MCP server with `vendor/bin/kirby-mcp`.

## Copy-paste request examples

Use these once your MCP client is connected to the server.

### Planning & content

> [!TIP]
> "Use the Kirby MCP to make a plan to..." is a reliable way to get your agent to use the tools and resources this MCP server provides. If you ask it to take action, it tends to edit files directly based on its training and skills.

```text
Use the Kirby MCP to make a plan to... build a contact form page.
```

`kirby_roots`, `kirby_blueprint_read`, `kirby_templates_index`, `kirby_snippets_index`, `kirby_controllers_index`, `kirby_render_page`, `kirby_dump_log_tail`, `kirby://config/{option}`

<details>
<summary>Agent response example:</summary>

```text
Here’s a Kirby MCP–driven plan for a new contact form page in this project:

- Confirm requirements: fields (name/email/message), spam protection (honeypot + csrf()), success UX (inline vs redirect), and whether to store submissions.
- Panel setup: reuse existing blueprint site/blueprints/pages/contact.yml; update site/blueprints/site.yml so the site “pages” section allows creating template contact.
- Create the page: add content/4_contact/contact.txt (or create via Panel once allowed) and fill recipient, subject, success, text.
- Frontend: add site/templates/contact.php + a snippet like site/snippets/contact-form.php (escape all echoed user input; include hidden CSRF field + honeypot).
- Controller: add site/controllers/contact.php to handle POST: check csrf(get('csrf')), validate with invalid(), then send mail via $kirby->email() using replyTo (never use user input as from) and an email template like site/templates/emails/contact.text.php (optional contact.html.php).
- Email config: add site/config/config.php email.transport/email.presets (current kirby://config/email is null).
- Verify with MCP: render the page via kirby_render_page(id='contact', noCache=true); debug with mcp_dump() + kirby_dump_log_tail(traceId=...); inspect blueprint/roots via kirby_blueprint_read(id='pages/contact') and kirby://roots.
```

</details>

---

```text
Show me the fields available on the home page blueprint and what they do using the MCP.
```

`kirby_blueprint_read`, `kirby_blueprints_index`

---

```text
Show me the current content of the about page.
```

`kirby_read_page_content`

---

```text
Append " with AI" to the title of the home page with Kirby MCP.
```

`kirby_read_page_content`, `kirby_update_page_content`, `kirby://field/text/update-schema`

### Resource shortcuts

> [!TIP]
> Either on its own or with a request, the resources can be used to quickly bring knowledge and runtime information into the current context of your agent.

```text
kirby://glossary/collection
```

`kirby://glossary/{term}`

---

```text
What is the kirby://config/debug for production?
```

`kirby://config/{option}`

### Search & docs

> [!TIP]
> The MCP server ships with a local knowledge base about Kirby. It consists of a glossary, common tasks, and update guides for content fields. This reduces the need to rely on external resources and is very fast.

```text
kirby search for collection filtering
```

`kirby_search`

---

> [!TIP]
> But sometimes you or your agent needs to dig deeper. That is why the MCP server also provides a fallback to the official Kirby search and docs (not including the forum). You can trigger it by mentioning `search online` in your request.

```text
kirby search online for panel permissions
```

`kirby_online`

---

> [!TIP]
> When you need to discover third-party plugins, you can also search the official Kirby plugin directory and fetch details from each plugin page.

```text
kirby search plugins online for e-commerce cart
```

`kirby_online_plugins`

---

> [!TIP]
> Your agent will use the next tool under the hood itself, but you can use it as well to quickly check what the MCP server knows about a given topic.

```text
What mcp tool should I use to... list plugins?
```

`kirby_tool_suggest`

### Inventory (runtime + filesystem)

```text
list blueprints, templates, snippets, collections, controllers, models, plugins, routes, roots
```

`kirby_blueprints_loaded`, `kirby_blueprints_index`, `kirby_templates_index`, `kirby_snippets_index`, `kirby_collections_index`, `kirby_controllers_index`, `kirby_models_index`, `kirby_plugins_index`, `kirby_routes_index`, `kirby_roots`

### Debug, tinker/eval and running commands

> [!IMPORTANT]
> The `kirby_eval` tool is disabled by default and CLI commands are protected by an allowlist/denylist, see config and security below.

```text
kirby MCP tinker $site->index()->count()
```

`kirby_eval`

---

```text
kirby MCP check query site.find('notes').unlisted.count
kirby MCP check query page.siblings.count (model: notes)
```

`kirby_query_dot`

---

```text
run kirby cli command uuid:populate
```

`kirby_run_cli_command`

---

```text
My home page renders incorrectly. Help me debug it with mcp_dump() to return the current $page object.
```

`kirby_render_page`, `kirby_dump_log_tail`, `kirby_templates_index`, `kirby_snippets_index`, `kirby_controllers_index`, `kirby_models_index`

## Capabilities

> [!INFO]
> `kirby_init` is required once per stdio handshake session before other tools. HTTP clients may use a fresh session per
> tool call, so HTTP calls do not require it; bearer/OAuth scopes remain authoritative. It remains recommended for
> audit/guidance and optional for stateless `2026-07-28` calls. Some capabilities require runtime wrappers because they
> query Kirby at runtime.

At initialization, the server tells the agent which tools/resources to use. The knowledge base cross-references them so the agent can find the next step.

Current inventory: 37 tools, 15 resources, 15 resource templates, 216 KB articles.

In global reference mode (`kirby-mcp --global`), the exposed surface is intentionally smaller: `kirby_init`, `kirby_search`, `kirby_online`, `kirby_online_plugins`, `kirby_tool_suggest`, and static reference resources/templates (`kirby://kb`, glossary, fields/sections, hooks, extensions, and update schemas).

Modern `2026-07-28` clients receive one-hour public cache hints for the global reference profile's deployment-static discovery surface and for bundled KB/reference index reads in either profile. Externally fetched docs and all project-specific results remain private and immediately stale.
Tool results that expose concrete `kirby://` references in structured fields also include navigable resource links for modern clients; URI templates remain structured references only.

<details>
<summary>🛠️ Tools</summary>

- `kirby_blueprint_read` — read a single blueprint by id
- `kirby_blueprints_index` — index blueprints, includes plugin-registered ones when runtime is installed
- `kirby_blueprints_loaded` — list blueprint ids loaded at runtime
- `kirby_cache_clear` — clear in-memory caches for this MCP session (StaticCache, config, composer, roots, tool index)
- `kirby_cli_version` — run `kirby version` and return stdout, stderr and exit code
- `kirby_composer_audit` — parse composer.json for scripts and quality tools
- `kirby_collections_index` — index named collections, includes plugin-registered ones when runtime is installed
- `kirby_controllers_index` — index controllers, includes plugin-registered ones when runtime is installed
- `kirby_online` — search official Kirby docs (online fallback) and optionally fetch markdown pages
- `kirby_online_plugins` — search the official Kirby plugins directory (online fallback) and optionally fetch plugin details
- `kirby_dump_log_tail` — tail `.kirby-mcp/dumps.jsonl` written by `mcp_dump()`
- `kirby_eval` — execute PHP in Kirby runtime for quick inspection, requires enable plus confirm
- `kirby_query_dot` — evaluate Kirby query language (dot-notation) strings, requires confirm and can be disabled via config
- `kirby_generate_ide_helpers` — generate regeneratable IDE helper files into `.kirby-mcp/`
- `kirby_ide_helpers_status` — report missing template/snippet PHPDoc `@var` hints for used Kirby globals + helper file freshness (mtime-based)
- `kirby_info` — project runtime info, composer audit and local environment detection
- `kirby_init` — session guidance plus project-specific audit; required once per stdio handshake session and recommended for HTTP
- `kirby_search` — search the bundled local Kirby knowledge base markdown files (preferred)
- `kirby_models_index` — index registered page models with class and file path info
- `kirby_plugins_index` — index loaded plugins, prefers runtime truth when installed
- `kirby_read_file_content` — read file content/metadata by id or uuid
- `kirby_read_page_content` — read page content by id or uuid
- `kirby_read_site_content` — read site content
- `kirby_read_user_content` — read user content by id or email
- `kirby_render_page` — render a page by id or uuid and return HTML plus errors
- `kirby_roots` — resolved Kirby roots via `kirby roots`
- `kirby_routes_index` — list registered routes with best-effort source location (config/plugin)
- `kirby_run_cli_command` — run a Kirby CLI command, guarded by an allowlist
- `kirby_runtime_install` — install project-local Kirby MCP runtime CLI commands into the project
- `kirby_runtime_status` — check whether runtime command wrappers are installed
- `kirby_snippets_index` — index snippets, includes plugin-registered ones when runtime is installed
- `kirby_templates_index` — index templates, includes plugin-registered ones when runtime is installed
- `kirby_tool_suggest` — suggest the best next Kirby MCP tool/resource for a task
- `kirby_update_file_content` — update file metadata/content, plus confirm (see `kirby://blueprint/file/update-schema` + `kirby://field/{type}/update-schema` for payload shapes)
- `kirby_update_page_content` — update page content, plus confirm (see `kirby://blueprint/page/update-schema` + `kirby://field/{type}/update-schema` for payload shapes)
- `kirby_update_site_content` — update site content, plus confirm (see `kirby://blueprint/site/update-schema` + `kirby://field/{type}/update-schema` for payload shapes)
- `kirby_update_user_content` — update user content, plus confirm (see `kirby://blueprint/user/update-schema` + `kirby://field/{type}/update-schema` for payload shapes)

Update tool `data` input accepts either a JSON object or a JSON-encoded object string for backward compatibility.
If your client supports MCP resource subscriptions, successful `kirby_update_*_content` writes emit `notifications/resources/updated` for subscribed content resources (`kirby://site/content`, `kirby://page/content/{...}`, `kirby://file/content/{...}`, `kirby://user/content/{...}`).
Confirm-gated tools (`kirby_update_*_content`, `kirby_eval`, `kirby_query_dot`) keep explicit `confirm=true`; clients with MCP elicitation support may present an inline confirmation prompt and continue on accept. Modern confirmations are bound to the complete operation inputs. If a retry changes those inputs, the stale response is ignored and the tool returns a safe preview with `confirmationStatus: "stale_input_ignored"` and `retryWithoutInputResponses: true`; start a fresh call to confirm the changed operation.

</details>

<details>

<summary>📚 Resources</summary>

> [!TIP]
> Call a resource to bring condensed knowledge into the current context of your agent.

Resources (read-only):

- `kirby://commands` — Kirby CLI command list, parsed from `kirby help`
- `kirby://composer` — composer audit, scripts and quality tooling
- `kirby://extensions` — Kirby plugin extensions list (links to `kirby://extension/{name}`)
- `kirby://fields` — Kirby Panel field types list (links to `kirby://field/{type}`)
- `kirby://fields/update-schema` — Kirby content field guides list (links to `kirby://field/{type}/update-schema`)
- `kirby://blueprints/update-schema` — Kirby blueprint update guides list (links to `kirby://blueprint/{type}/update-schema`)
- `kirby://glossary` — Kirby glossary terms list (links to `kirby://glossary/{term}`)
- `kirby://kb` — bundled KB index (links to `kirby://kb/{path}`)
- `kirby://hooks` — Kirby hook names list (links to `kirby://hook/{name}`)
- `kirby://info` — project runtime info, composer audit and local environment detection
- `kirby://roots` — Kirby roots discovered via CLI, respects configured host
- `kirby://sections` — Kirby Panel section types list (links to `kirby://section/{type}`)
- `kirby://tools` — weighted keyword index for Kirby MCP tools/resources/templates
- `kirby://uuid/new` — generate a new Kirby UUID string (respects `content.uuid` format)

Resource templates (dynamic):

- `kirby://blueprint/{encodedId}` — read a blueprint by URL-encoded id, e.g. `pages%2Fhome`
- `kirby://cli/command/{command}` — parsed `kirby <command> --help` output, e.g. `backup` or `uuid:generate`
- `kirby://config/{option}` — read a Kirby config option by dot path
- `kirby://extension/{name}` — Kirby extension reference markdown from getkirby.com, e.g. `commands` or `darkroom-drivers`
- `kirby://field/{type}` — Kirby Panel field reference markdown from getkirby.com, e.g. `blocks` or `email`
- `kirby://field/{type}/update-schema` — bundled content field guide from `kb/update-schema/{type}.md`
- `kirby://blueprint/{type}/update-schema` — bundled blueprint update guide from `kb/update-schema/blueprint-{type}.md`
- `kirby://glossary/{term}` — read a bundled Kirby glossary entry by term, e.g. `api` or `kql`
- `kirby://kb/{path}` — read a bundled KB document by path (relative to `kb/`, no `.md`)
- `kirby://hook/{name}` — Kirby hook reference markdown from getkirby.com, e.g. `file.changeName:after` or `file-changename-after`
- `kirby://file/content/{encodedIdOrUuid}` — read file content/metadata by URL-encoded id or uuid
- `kirby://page/content/{encodedIdOrUuid}` — read page content by URL-encoded id or uuid
- `kirby://section/{type}` — Kirby Panel section reference markdown from getkirby.com, e.g. `fields` or `files`
- `kirby://site/content` — read site content
- `kirby://susie/{phase}/{step}` — easter egg resource template
- `kirby://user/content/{encodedIdOrEmail}` — read user content by URL-encoded id or email

</details>

## Skills

Bundled Skills live in `vendor/bnomei/kirby-mcp/skills` after installation. Copy them into your agent’s local skills folder using the **Client setup** instructions below.

- `kirby-project-tour` — Project inventory and orientation (roots, blueprints, plugins) with next-step recommendations.
- `kirby-content-migration` — Safe content migrations with runtime read/update tools and update schemas.
- `kirby-scaffold-page-type` — Scaffold a page type (blueprint + template + optional controller/model) using project conventions.
- `kirby-routing-and-representations` — Custom routes, redirects, and content representations (.json/.xml/.rss).
- `kirby-collections-and-navigation` — Listings, pagination, search, filtering/sorting/grouping, and navigation menus.
- `kirby-panel-and-blueprints` — Blueprint design, Panel UX, `extends`, and custom areas/fields/sections.
- `kirby-plugin-development` — Reusable plugins with hooks/extensions, KirbyTags, blocks, and shared controllers/templates.
- `kirby-headless-api` — Headless API setup with Kirby API, KQL, and JSON representations.
- `kirby-i18n-workflows` — Language config, translation keys, localized labels, and import/export workflows.
- `kirby-security-and-auth` — Login/roles/permissions, access restriction, and protected downloads.
- `kirby-performance-and-media` — Cache tuning, CDN/media routing, responsive images, and lazy loading.
- `kirby-debugging-and-tracing` — Render reproduction, runtime tracing with `mcp_dump`, and code-path discovery.
- `kirby-ide-support` — IDE helper status plus minimal PHPDoc/type-hint improvements.
- `kirby-upgrade-and-maintenance` — Safe Kirby upgrades with composer audit, plugin checks, and verification.
- `kirby-forms-and-frontend-actions` — Contact forms, uploads, emails, and frontend page creation with validation/CSRF.

## Client setup

> [!NOTE]
> The `--project` flag is optional when you run the server from the Kirby project root.
> Use it (or `KIRBY_MCP_PROJECT_ROOT`) only for project-local MCP servers that should inspect a specific Kirby project.
> Command-based stdio is the default and recommended setup for local IDE/agent use.
> `kirby-mcp --global` is a separate projectless reference server and must not be combined with `--project`.

### Cursor

Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):

```json
{
  "mcpServers": {
    "kirby-reference": {
      "command": "kirby-mcp",
      "args": ["--global"]
    },
    "kirby-project": {
      "command": "/absolute/path/to/kirby-project/vendor/bin/kirby-mcp"
    }
  }
}
```

Use `kirby-reference` for docs/KB research that is always available. Use `kirby-project` only when that specific Kirby project should be inspectable or mutable.

### Claude Code

From the Kirby project directory:

```bash
claude mcp add kirby -- vendor/bin/kirby-mcp
```

Global reference server:

```bash
claude mcp add kirby-reference -- kirby-mcp --global
```

Or explicitly:

```bash
claude mcp add kirby -- vendor/bin/kirby-mcp --project=/absolute/path/to/kirby-project
```

Copy bundled Skills (personal scope):

```bash
mkdir -p ~/.claude/skills
rsync -a vendor/bnomei/kirby-mcp/skills/ ~/.claude/skills/
```

Restart Claude Code after copying (use `.claude/skills/` instead for repo-scoped skills).

### Codex CLI

From the Kirby project directory:

```bash
codex mcp add kirby -- vendor/bin/kirby-mcp
```

Global reference server:

```bash
codex mcp add kirby-reference -- kirby-mcp --global
```

Or explicitly:

```bash
codex mcp add kirby -- vendor/bin/kirby-mcp --project=/absolute/path/to/kirby-project
```

Copy bundled Skills (user scope):

```bash
mkdir -p ~/.codex/skills
rsync -a vendor/bnomei/kirby-mcp/skills/ ~/.codex/skills/
```

Restart Codex CLI after copying.

### Manual

Start the server (point it at a composer-based Kirby project):

- From the Kirby project root: `vendor/bin/kirby-mcp`
- Or explicitly: `vendor/bin/kirby-mcp --project=/absolute/path/to/kirby-project`
- Or as the projectless global reference server: `kirby-mcp --global`

### HTTP transport (optional)

HTTP is disabled by default. `vendor/bin/kirby-mcp` continues to run stdio unless you add the Kirby
route and set `"http.enabled": true` in `.kirby-mcp/mcp.json` or environment variables.

Modern resource update subscriptions use a bounded, 120-second local-filesystem bus under `.kirby-mcp/http-notifications` so separate same-host HTTP workers can communicate. Each subscribed URI requires the same bearer scope as reading that resource; project content subscriptions require `kirby-mcp:runtime`. This is not multi-host/NFS storage and does not promise durability or exactly-once delivery. Stateless dump calls must pass an explicit render `traceId` or request `path`; only handshake sessions provide last-trace convenience. Confirmation elicitation is a user-safety interaction, while HTTP bearer scopes are authorization; explicit `confirm=true` remains supported.

> [!NOTE]
> Remote HTTP follows the standard MCP pattern: HTTPS `/mcp`, Bearer/OAuth auth, and MCP metadata
> discovery. Claude Code and Claude Desktop/Claude.ai custom connectors are the primary tested
> targets. Other MCP-compatible clients may work if they support remote HTTP MCP and the configured
> auth mode. OpenAI/ChatGPT uses MCP through Responses API tools and ChatGPT Apps/MCP Apps, not the
> Claude custom-connector URL flow documented here.

Use one auth mode:

| Mode           | Use for                                                      |
| -------------- | ------------------------------------------------------------ |
| `shared-token` | Local loopback development with a client on the same host.   |
| `remote-token` | Public HTTPS routes for clients that can send Bearer tokens. |
| `oauth`        | Claude Desktop/Claude.ai custom connectors or OAuth clients. |

For a Kirby route, install this package as a production dependency:

```bash
composer require bnomei/kirby-mcp
```

Do not install it with `composer require --dev` if your `/mcp` route should work in production;
the production PHP runtime must be able to autoload `Bnomei\KirbyMcp\Mcp\KirbyMcpRoutes`.

Add these routes to your Kirby config, usually `site/config/config.php`:

```php
<?php

use Bnomei\KirbyMcp\Mcp\KirbyMcpRoutes;

return [
    'routes' => [
        ...KirbyMcpRoutes::routes(),
    ],
];
```

If your config already defines `routes`, spread these entries into the existing routes array
instead of replacing it. No special Nginx location or `vendor/bin/kirby-mcp` proxy is required.

The route helper adds `/mcp` plus the optional OAuth metadata, registration, authorize/token, JWKS,
and login routes. If you change `http.path`, pass the same path to the route helper:

```php
'routes' => [
    ...KirbyMcpRoutes::routes('/custom-mcp'),
],
```

If you also change the built-in OAuth provider path, pass it as a named argument:

```php
'routes' => [
    ...KirbyMcpRoutes::routes('/custom-mcp', oauthPath: '/custom-mcp/oauth'),
],
```

HTTP tool calls do not require a preceding `kirby_init`: remote clients may create a fresh handshake session for every
call. `kirby_init` remains available and recommended for project audit and guidance. Every HTTP operation is still
authorized independently through its bearer/OAuth scopes.

SSE GET streams default to a 300-second package lifetime. The transport asks PHP to extend its execution deadline to
slightly beyond that lifetime, while the package loop remains the authoritative bound. If the host disables
`set_time_limit()`, configure PHP/FrankenPHP `max_execution_time` above the `sseMaxSeconds` value passed to
`KirbyMcpRoutes::routes()`.

Put the JSON examples below in your Kirby project’s MCP config file:`.kirby-mcp/mcp.json`.

> [!WARNING]
> All `/mcp` requests require `Authorization: Bearer ...`; query-string credentials are rejected.
> Public route requests require HTTPS, requests with an `Origin` header must match
> `http.allowedOrigins`, and every operation is scope-checked. If the route is registered but
> `http.enabled` is false, it returns 404.

#### Local loopback token

Use `shared-token` only for local development from the same machine:

```json
{
  "http": {
    "enabled": true,
    "path": "/mcp",
    "allowedOrigins": ["http://127.0.0.1:3000"],
    "auth": {
      "mode": "shared-token",
      "token": "replace-with-a-long-random-secret",
      "scopes": ["kirby-mcp:read", "kirby-mcp:runtime", "kirby-mcp:write", "kirby-mcp:execute", "kirby-mcp:admin"]
    }
  }
}
```

The Kirby route rejects shared-token requests unless PHP reports `REMOTE_ADDR` as loopback and the request host is a real loopback host (`localhost`, `::1`, or a valid IPv4 literal in `127.0.0.0/8`).

#### Remote bearer token (recommended)

Use `remote-token` for public HTTPS routes when the client can send a static Bearer token:

```json
{
  "http": {
    "enabled": true,
    "path": "/mcp",
    "allowedOrigins": [],
    "auth": {
      "mode": "remote-token",
      "tokens": [
        {
          "id": "claude-code",
          "hash": "sha256:replace-with-sha256-token-hash",
          "userId": "editor-user",
          "scopes": ["kirby-mcp:read", "kirby-mcp:runtime", "kirby-mcp:write", "kirby-mcp:execute", "kirby-mcp:admin"]
        }
      ]
    }
  }
}
```

Local CLI clients usually send no `Origin` header, so omit `allowedOrigins` or leave it empty. Add
exact origins only for browser or webview clients that send `Origin`, for example
`http://localhost:5173`.

Generate the token hash:

```bash
php -r 'echo "sha256:" . hash("sha256", $argv[1]) . PHP_EOL;' 'replace-with-a-long-random-secret'
```

Or provide the raw token through the environment:

```bash
KIRBY_MCP_HTTP_AUTH_MODE=remote-token
KIRBY_MCP_HTTP_REMOTE_TOKEN=replace-with-a-long-random-secret
KIRBY_MCP_HTTP_REMOTE_TOKEN_ID=claude-code
KIRBY_MCP_HTTP_REMOTE_TOKEN_USER_ID=editor-user
KIRBY_MCP_HTTP_REMOTE_TOKEN_SCOPES=kirby-mcp:read,kirby-mcp:runtime
```

Use OAuth instead for Claude Desktop/Claude.ai custom connectors.

#### OAuth auth (for Claude Desktop/Ai)

Use `oauth` for Claude Desktop/Claude.ai custom connectors. No separate OAuth server package is
required for the built-in Claude flow.

**Kirby users and permissions come first:** OAuth authenticates the connecting user, while every
remote token names one existing Kirby user. Both modes apply the hierarchical Kirby MCP capability
map; dedicated page, file, site, and user updates additionally run as that Kirby user. See
[Kirby permissions and limits](#kirby-permissions-and-limits) and [the complete capability map](docs/permissions.md).

1. Run `vendor/bin/kirby-mcp install` (or `update` after upgrading) to install the runtime commands and permission adapter.
2. Register `KirbyMcpRoutes::routes()`.
3. Enable the config below and configure the connecting user's role permissions.
4. Add a Claude custom connector with MCP server URL `https://example.com/mcp`.
5. Add the consent snippet only if you want a custom approval screen.

```json
{
  "http": {
    "enabled": true,
    "path": "/mcp",
    "allowedOrigins": ["https://claude.ai"],
    "auth": {
      "mode": "oauth",
      "scopes": ["kirby-mcp:read", "kirby-mcp:runtime", "kirby-mcp:write", "kirby-mcp:execute", "kirby-mcp:admin"]
    },
    "oauthProvider": {
      "enabled": true,
      "path": "/mcp/oauth",
      "consent": "snippet",
      "role": "admin"
    }
  }
}
```

With `oauthProvider.enabled=true`, the route derives issuer, audience/resource, and JWKS URL from
the incoming HTTPS request unless you set `http.auth.issuer`, `http.auth.audience`, or
`http.auth.jwksUri`. Provider state is stored under `.kirby-mcp/oauth`, not in Kirby cache.

The example admits only administrators. To connect editors instead, set `http.oauthProvider.role` to
their existing Kirby role name (for example `editor`), or `"*"` to admit any authenticated Kirby user.
This setting controls who can authorize a connection; it does not assign or change their Kirby role.

> [!IMPORTANT]
> Consent defaults to `snippet`. A logged-in Kirby user must approve or deny the client before Claude
> gets a token. If the user is not logged in, Kirby MCP stores the authorize request under
> `.kirby-mcp/oauth/sessions` and redirects to Kirby's Panel login, respecting its configured
> login methods and two-factor authentication. Panel must be enabled. A fixed `/mcp/oauth/resume`
> route restores the request from the same browser's Kirby session (Panel strips return-URL query
> strings). Login handoffs expire after ten minutes; starting another handoff in the same browser
> replaces the previous one. Resumed requests always require explicit consent, even with `auto`.
> Use `auto` only for trusted private deployments. Recopy the route bundle when upgrading from
> the former `/mcp/oauth/login` credential form.

For a custom approval screen, create a new snippet. The default snippet name is `kirby-mcp/oauth-consent`, which maps to `site/snippets/kirby-mcp/oauth-consent.php` in a Kirby project. The snippet receives `client`, `scopes`, `user`, `approveUrl`, `denyUrl`, and `error`:

```php
<?php
$clientName = (string) ($client['client_name'] ?? $client['client_id'] ?? 'OAuth client');
$userEmail = (string) ($user?->email() ?? 'Kirby user');
?>

<?php if ($error !== null): ?>
<p><?= esc((string) $error) ?></p>
<?php endif ?>

<form method="post" action="<?= esc((string) $approveUrl, 'attr') ?>">
  <h1>Authorize <?= esc($clientName) ?></h1>
  <p><?= esc($userEmail) ?></p>
  <ul>
    <?php foreach ($scopes as $scope): ?>
    <li><?= esc((string) $scope) ?></li>
    <?php endforeach ?>
  </ul>
  <input type="hidden" name="csrf" value="<?= esc((string) csrf(), 'attr') ?>">
  <button type="submit" name="approve" value="1">Approve</button>
  <button type="submit" name="deny" value="1" formaction="<?= esc((string) $denyUrl, 'attr') ?>">Deny</button>
</form>
```

The snippet must submit `POST` back to the provided authorize URL, include a `csrf` field generated
by Kirby's `csrf()` helper, and submit either `approve=1` or `deny=1`.

##### Custom OAuth/OIDC issuer

If you already have, or want to build, a separate OAuth/OIDC authorization server, keep
`oauthProvider.enabled` false and configure the issuer, audience/resource, and JWKS URI yourself:

```json
{
  "http": {
    "enabled": true,
    "path": "/mcp",
    "allowedOrigins": ["https://client.example"],
    "auth": {
      "mode": "oauth",
      "issuer": "https://auth.example.test",
      "audience": "https://example.test/mcp",
      "jwksUri": "https://auth.example.test/.well-known/jwks.json",
      "scopes": ["kirby-mcp:read", "kirby-mcp:runtime", "kirby-mcp:write", "kirby-mcp:execute", "kirby-mcp:admin"]
    }
  }
}
```

OAuth mode validates JWT access tokens by issuer, audience/resource, JWKS signature, expiry, and
operation scopes. Kirby MCP validates the resulting JWTs for this mode; it does not run your custom
authorization server for you. A package such as `league/oauth2-server` can be useful if you build
that issuer yourself, but it is not used by the built-in Claude OAuth provider.

For all remote operations, an external issuer must set JWT `sub` to the exact ID of an existing Kirby user
in this project (not their email or an external account ID). There is no automatic account mapping
or provisioning. Missing subjects and unknown/deleted users are rejected.

HTTP tokens are scope-checked per operation. Available scope names are:

- `kirby-mcp:read` for read-only tools and resources.
- `kirby-mcp:runtime` for runtime inspection that executes Kirby CLI wrappers.
- `kirby-mcp:write` for content/file/user/site mutations, still requiring confirmation.
- `kirby-mcp:execute` for query/eval-style operations, still requiring enablement and confirmation.
- `kirby-mcp:admin` for administrative runtime actions.

Composer installs the HTTP/JWT runtime libraries needed by Kirby MCP itself as direct package
dependencies; upgrading this package is enough for consumers unless your deployment pins Composer
with `--no-update`.

## IDE helpers (optional, for humans)

The agent can both check and generate IDE helpers for your project: `kirby_ide_helpers_status` and `kirby_generate_ide_helpers`. You can also use the CLI commands yourself.

- Check baseline + freshness: `vendor/bin/kirby-mcp ide:status` (use `--details` and `--limit=N` for more output)
- Generate regeneratable helper files: `vendor/bin/kirby-mcp ide:generate` (default is `--dry-run`; add `--write` to create files)
- JSON output: `--json` (MCP markers) or `--raw-json` (plain JSON)

## What the MCP server does (and doesn’t)

- Provides MCP tools/resources for project inspection (blueprints, templates/snippets/collections, controllers/models, plugins, routes, roots).
- Fetches official Kirby reference docs and ships a local Markdown knowledge base (`kb/`) for fast lookups.
- Doesn’t modify your content by default; write-capable actions run by the MCP are guarded and require explicit opt-in/confirmation. But your agent still can do whatever you allow it to!
- Only supports composer-based Kirby projects (Kirby CLI is used for many capabilities).

## Security model

### Kirby permissions and limits

OAuth and remote-token connections resolve an existing Kirby user and apply the registered
`bnomei.kirby-mcp` capability map to discovery and every operation. Every parent and leaf permission
must be `true`; all entries default to `false` for custom roles, while Kirby's native `admin` role
remains unrestricted. The four dedicated content-update tools also execute as that user, so Kirby's
native mutation checks and hooks apply.

Use [Kirby's permission configuration](https://getkirby.com/docs/guide/users/permissions):

- Define role permissions in `site/blueprints/users/<role>.yml`.
- Use model blueprint `options` and `before` hooks for project-specific rules.
- Use a custom role for restricted users; Kirby's `admin` role remains unrestricted.

HTTP scopes remain coarse tool-access gates, and write confirmations remain required. Neither replaces
Kirby permissions. Local stdio and loopback shared-token connections remain trusted-operator access.
See [Remote MCP permissions](docs/permissions.md) for role configuration and the complete map.

**This is capability gating, not per-target isolation.** Granting a read capability permits every target
supported by it. Enabled and permitted eval or generic CLI execution is trusted execution and can bypass
other permissions. Identity, role, and account revocation are rechecked per HTTP request, but an already
open streaming response is not continuously re-authorized. Do not use this server to isolate untrusted
users to a private subset of a site.

### Additional execution and transport controls

- `kirby_run_cli_command` is guarded by an allowlist; extend it via `.kirby-mcp/mcp.json` (`cli.allow`, `cli.allowWrite`) and block via `cli.deny`.
- Write-capable actions require explicit opt-in (e.g. `allowWrite=true` or `confirm=true`, depending on the tool).
- `kirby_eval` is disabled by default; enable via `KIRBY_MCP_ENABLE_EVAL=1` or `.kirby-mcp/mcp.json` (`{"eval":{"enabled":true}}`) and still requires per-call confirmation (`confirm=true` or client-side elicitation).
- `kirby_query_dot` is enabled by default; disable via `.kirby-mcp/mcp.json` (`{"query":{"enabled":false}}`) and still requires per-call confirmation (`confirm=true` or client-side elicitation).
- HTTP transport is disabled by default and must never be exposed without Bearer-token authorization.
- HTTP shared-token auth is limited to local development. Keep the token outside source control; the Kirby route rejects shared-token requests when `REMOTE_ADDR` is not loopback or the request host is not a real loopback host.
- HTTP remote-token auth is explicit public bearer-token auth for header-capable clients. Every token requires an existing Kirby `userId`. Store hashes in config, keep raw tokens in environment/secret storage, require HTTPS for non-loopback route requests, and scope tokens tightly.
- OAuth remains the preferred production path for clients that need an interactive auth flow, including Claude Desktop and Claude.ai custom connectors. The optional built-in provider is disabled by default and writes only to `.kirby-mcp/oauth`.
- HTTP validates `Origin` before MCP protocol handling and rejects missing, malformed, expired, invalid, or insufficient-scope tokens before tool/resource side effects.
- HTTP exposes only the configured MCP route path, `/mcp` by default, for MCP traffic.

## What `install` / `update` change in your project

`vendor/bin/kirby-mcp install`:

- Creates `.kirby-mcp/mcp.json` if neither `.kirby-mcp/mcp.json` nor `.kirby-mcp/config.json` exist.
- Copies runtime command wrappers into the project’s Kirby commands root (usually `site/commands/mcp/`).
- Copies the tiny plugin adapter `index.php`, static Panel assets (`index.js`, `index.css`), and generates
  its metadata-only `composer.json` in the resolved plugins root (usually `site/plugins/kirby-mcp/`).
  The server and its dependencies remain a library in `vendor/`. The adapter registers permissions
  and an authenticated Panel activity API, not the public MCP transport route.
- Use `--force` to overwrite existing wrapper files.

`vendor/bin/kirby-mcp update`:

- Overwrites the runtime wrappers and refreshes the copied plugin adapter metadata (use after upgrading this package).
- Creates `.kirby-mcp/mcp.json` only if missing; it won’t overwrite an existing config.

To remove everything:

- Delete the runtime wrappers folder (`site/commands/mcp/` in most projects).
- Delete the adapter folder (`site/plugins/kirby-mcp/` in most projects).
- Optionally delete `.kirby-mcp/` (config + caches + optional helper files).

## Optional Panel activity indicator

Enable this in your project's `.kirby-mcp/mcp.json` (or existing `config.json`):

```json
{
  "activity": { "enabled": true }
}
```

Run `vendor/bin/kirby-mcp update` after upgrading to copy the Panel assets, then reload the Panel.
No frontend build, custom Panel stylesheet, public status URL, or additional secret is needed.

Panel admins see a tiny, top-center robot indicator after a successful MCP tool call or resource read:

| Time since activity               | Appearance   |
| --------------------------------- | ------------ |
| Under 30 seconds                  | Orange       |
| 30 seconds–2 minutes              | Muted orange |
| 2–5 minutes                       | Grey         |
| 5 minutes or more, or no activity | Hidden       |

Hover, focus, or tap the indicator for the elapsed time. It polls every 15 seconds while the Panel tab
is visible and hides on authentication/network failure. This indicates **recent successful activity**,
not an open connection, a running operation, or a count of agents. Initialization, discovery, ping,
failed requests, and Panel polling do not refresh it; the `kirby_init` tool itself does count.

The feature is disabled by default. When enabled, project-local stdio and HTTP calls share one timestamp
at `.kirby-mcp/activity`; global reference mode never records project activity. The MCP process and PHP
web process must share this project directory and have the necessary file permissions. This is a
best-effort, single-filesystem signal, not an audit log or multi-server presence service. Keep
`.kirby-mcp/` outside the public document root or deny web access to it, as for the other MCP state files.

`GET /api/kirby-mcp/activity` uses Kirby's normal API authentication and requires a Kirby admin. It
returns only `state` and `ageSeconds`, never identities, tool names, arguments, or content. There is
no public endpoint or token embedded in CSS. The Panel integration uses Kirby's `created` hook and
an isolated DOM element; it does not replace core Panel components.

## Debug dumps (`mcp_dump`)

This package provides a lightweight `mcp_dump()` helper that appends JSONL to `.kirby-mcp/dumps.jsonl` in the project root.

**Secret redaction:** By default, dump output is scanned for sensitive data (API keys, tokens, passwords, IPs) and redacted before writing. This protects against accidentally leaking secrets. Configure via `dumps.secretPatterns` in `.kirby-mcp/mcp.json`:

```json
{
  "dumps": {
    "secretPatterns": []
  }
}
```

- Omit `secretPatterns` → use built-in patterns (OpenAI/Anthropic/GitHub/Stripe/AWS keys, JWTs, Bearer tokens, IPs, etc.)
- Set to `[]` → disable redaction entirely
- Set to `["/pattern1/", "/pattern2/"]` → use only your custom regex patterns

Typical workflow for your coding agent:

- Add `mcp_dump($anything)` (optionally chain `->green()`, `->label('...')`, `->caller()`, `->trace()`, `->pass($value)`) anywhere in templates/snippets/controllers.
- Call `kirby_render_page` (it returns a `traceId`).
- Call `kirby_dump_log_tail(traceId=...)` to retrieve the captured dump events for that render.

## Configuration

Project config lives in `.kirby-mcp/mcp.json` (or `.kirby-mcp/config.json`) in the Kirby project root.
It is created by `vendor/bin/kirby-mcp install` if missing.

Kirby host selection:

- By default, Kirby CLI runs with no `KIRBY_HOST` override.
- To use host-specific Kirby config, set `KIRBY_MCP_HOST` (or `KIRBY_HOST`) when starting the MCP server, or set `kirby.host` in `.kirby-mcp/mcp.json`:
  - `{"kirby":{"host":"localhost"}}`

| Option                              | Type       | Default                   | Description                                                                                                                                                                                                                          |
| ----------------------------------- | ---------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cache.ttlSeconds`                  | `int`      | `60`                      | In-memory cache TTL (seconds) for read-only resources like `kirby://commands` and `kirby://cli/command/{command}` plus a few internal caches (roots inspection, completions); set to `0` to disable caching.                         |
| `docs.ttlSeconds`                   | `int`      | `86400`                   | In-memory cache TTL (seconds) for fetched getkirby.com markdown docs (e.g. `kirby://field/{type}` and `kirby://section/{type}`); set to `0` to disable caching.                                                                      |
| `cli.allow`                         | `string[]` | `[]`                      | Additional allowlist patterns for `kirby_run_cli_command` (supports `*` wildcard, e.g. `plugin:*`).                                                                                                                                  |
| `cli.allowWrite`                    | `string[]` | `[]`                      | Additional allowlist patterns for write-capable commands; requires `allowWrite=true` when calling `kirby_run_cli_command` (supports `*`).                                                                                            |
| `cli.deny`                          | `string[]` | `[]`                      | Deny patterns that always block commands, even if allowlisted (supports `*`).                                                                                                                                                        |
| `dumps.enabled`                     | `bool`     | `true`                    | Enable/disable `mcp_dump()` writes to `.kirby-mcp/dumps.jsonl`.                                                                                                                                                                      |
| `dumps.maxBytes`                    | `int`      | `2097152`                 | Max size for `.kirby-mcp/dumps.jsonl` written by `mcp_dump()`. When the next write would exceed it, the log is compacted by keeping the newest half of lines, then the new entry is appended.                                        |
| `dumps.secretPatterns`              | `string[]` | (defaults)                | Regex patterns for secret redaction in dump logs. Omit to use defaults (API keys, tokens, IPs, etc.), set to `[]` to disable masking, or provide custom patterns.                                                                    |
| `ide.typeHintScanBytes`             | `int`      | `16384`                   | Max bytes to read from controller/model files when detecting Kirby IDE baseline type hints (see `kirby_ide_helpers_status`).                                                                                                         |
| `kirby.host`                        | `string`   | `null`                    | Default Kirby host to pass as `KIRBY_HOST` to the Kirby CLI (affects host-specific config like `config.{host}.php`).                                                                                                                 |
| `eval.enabled`                      | `bool`     | `false`                   | Enable `kirby_eval` / `kirby mcp:eval` (still requires explicit confirmation per call).                                                                                                                                              |
| `query.enabled`                     | `bool`     | `true`                    | Enable `kirby_query_dot` / `kirby mcp:query:dot` (still requires explicit confirmation per call).                                                                                                                                    |
| `http.enabled`                      | `bool`     | `false`                   | Enable the optional Streamable HTTP MCP transport. Stdio remains the default when this is false or unset.                                                                                                                            |
| `http.host`                         | `string`   | `127.0.0.1`               | Bind host for the low-level HTTP listener/config check. Shared-token mode requires a real loopback host; the Kirby route adapter separately rejects shared-token auth unless both `REMOTE_ADDR` and the request host are loopback.   |
| `http.port`                         | `int`      | `8765`                    | Bind port for the low-level HTTP listener/config check. The Kirby route adapter does not use this field.                                                                                                                             |
| `http.path`                         | `string`   | `/mcp`                    | Single MCP endpoint path for Streamable HTTP requests. Match this with the copied Kirby route pattern.                                                                                                                               |
| `http.allowedOrigins`               | `string[]` | `[]`                      | Allowed browser origins for HTTP mode. Configure the exact client origins you expect.                                                                                                                                                |
| `http.auth.mode`                    | `string`   | `null`                    | Required when HTTP is enabled: `oauth` for JWT validation, `remote-token` for public bearer-token clients that can send headers, or `shared-token` for loopback local development.                                                   |
| `http.auth.token`                   | `string`   | `null`                    | Shared-token secret for local development. Prefer `KIRBY_MCP_HTTP_TOKEN` so secrets stay out of source control.                                                                                                                      |
| `http.auth.tokens`                  | `array`    | `[]`                      | Remote-token records. Each needs `id`, `hash` (`sha256:<64-hex>`), `userId` (an existing Kirby user ID), and optional per-token `scopes`.                                                                                            |
| `http.auth.issuer`                  | `string`   | `null`                    | OAuth issuer expected in JWT access tokens.                                                                                                                                                                                          |
| `http.auth.audience`                | `string`   | `null`                    | OAuth audience/resource expected in JWT access tokens, usually the MCP resource URL.                                                                                                                                                 |
| `http.auth.jwksUri`                 | `string`   | `null`                    | OAuth JWKS URI used to verify access-token signatures.                                                                                                                                                                               |
| `http.auth.scopes`                  | `string[]` | `[]`                      | Accepted operation scopes such as `kirby-mcp:read`, `kirby-mcp:runtime`, `kirby-mcp:write`, `kirby-mcp:execute`, and `kirby-mcp:admin`.                                                                                              |
| `http.oauthProvider.enabled`        | `bool`     | `false`                   | Enable the built-in OAuth authorization server for Claude Desktop/Claude.ai custom connectors.                                                                                                                                       |
| `http.oauthProvider.path`           | `string`   | `/mcp/oauth`              | Built-in OAuth provider route prefix. Match this with the fourth argument to `KirbyMcpRoutes::routes()` if you customize it.                                                                                                         |
| `http.oauthProvider.consent`        | `string`   | `snippet`                 | Consent mode: `snippet`, `always`, `remember`, or `auto`. `auto` skips explicit consent for logged-in Kirby users and should only be used for trusted private deployments.                                                           |
| `http.oauthProvider.consentSnippet` | `string`   | `kirby-mcp/oauth-consent` | Kirby snippet used when `consent` is `snippet`.                                                                                                                                                                                      |
| `http.oauthProvider.role`           | `string`   | `admin`                   | Panel role required to authorize MCP OAuth clients. A logged-in user without this role is denied (`access_denied`), so low-privilege accounts cannot mint tokens. Use `*` to allow any authenticated Panel user (loopback/dev only). |

Environment variables:

| Env var                                         | Description                                                                                                                           |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `KIRBY_MCP_PROJECT_ROOT`                        | Project root (overrides auto-detection).                                                                                              |
| `KIRBY_MCP_KIRBY_BIN`                           | Path to `vendor/bin/kirby` (overrides binary resolution).                                                                             |
| `KIRBY_MCP_PHP_BINARY`                          | PHP CLI binary for wrapped Kirby calls. Overrides `PHP_BINARY` and `PHP_BINDIR/php`; set it when automatic resolution is unavailable. |
| `KIRBY_MCP_HOST` / `KIRBY_HOST`                 | Kirby host override (takes precedence over config).                                                                                   |
| `KIRBY_MCP_DUMPS_ENABLED`                       | Override `dumps.enabled` (`1/0`, `true/false`, `on/off`).                                                                             |
| `KIRBY_MCP_ENABLE_EVAL`                         | Enable eval override (takes precedence over config; still needs confirmation).                                                        |
| `KIRBY_MCP_ENABLE_QUERY`                        | Enable query eval override (takes precedence over config; still needs confirmation).                                                  |
| `KIRBY_MCP_HTTP_ENABLED`                        | Enable optional HTTP transport (`1/0`, `true/false`, `on/off`).                                                                       |
| `KIRBY_MCP_HTTP_HOST`                           | HTTP bind host for the low-level listener/config check; defaults to `127.0.0.1`.                                                      |
| `KIRBY_MCP_HTTP_PORT`                           | HTTP bind port for the low-level listener/config check; defaults to `8765`.                                                           |
| `KIRBY_MCP_HTTP_PATH`                           | HTTP MCP endpoint path; defaults to `/mcp`; match this with the Kirby route pattern.                                                  |
| `KIRBY_MCP_HTTP_ALLOWED_ORIGINS`                | Comma-separated allowed origins for HTTP requests.                                                                                    |
| `KIRBY_MCP_HTTP_AUTH_MODE`                      | HTTP auth mode: `oauth`, `remote-token`, or `shared-token`.                                                                           |
| `KIRBY_MCP_HTTP_TOKEN`                          | Shared-token bearer secret for loopback local development.                                                                            |
| `KIRBY_MCP_HTTP_REMOTE_TOKEN`                   | Raw remote-token bearer secret for public HTTPS routes; prefer secret storage.                                                        |
| `KIRBY_MCP_HTTP_REMOTE_TOKEN_HASH`              | Remote-token hash in `sha256:<64-hex>` format.                                                                                        |
| `KIRBY_MCP_HTTP_REMOTE_TOKEN_ID`                | Remote-token identifier used in auth metadata; defaults to `env`.                                                                     |
| `KIRBY_MCP_HTTP_REMOTE_TOKEN_USER_ID`           | Existing Kirby user ID required for the environment remote token.                                                                     |
| `KIRBY_MCP_HTTP_REMOTE_TOKEN_SCOPES`            | Comma-separated scopes for the environment remote token.                                                                              |
| `KIRBY_MCP_HTTP_OAUTH_ISSUER`                   | OAuth JWT issuer.                                                                                                                     |
| `KIRBY_MCP_HTTP_OAUTH_AUDIENCE`                 | OAuth JWT audience/resource.                                                                                                          |
| `KIRBY_MCP_HTTP_OAUTH_JWKS_URI`                 | OAuth JWKS URI for JWT signature validation.                                                                                          |
| `KIRBY_MCP_HTTP_OAUTH_PROVIDER_ENABLED`         | Enable the built-in OAuth provider (`1/0`, `true/false`, `on/off`).                                                                   |
| `KIRBY_MCP_HTTP_OAUTH_PROVIDER_PATH`            | Built-in OAuth provider route prefix; defaults to `/mcp/oauth`.                                                                       |
| `KIRBY_MCP_HTTP_OAUTH_PROVIDER_CONSENT`         | Built-in OAuth provider consent mode: `auto`, `remember`, `always`, or `snippet`.                                                     |
| `KIRBY_MCP_HTTP_OAUTH_PROVIDER_CONSENT_SNIPPET` | Kirby snippet used when provider consent mode is `snippet`.                                                                           |
| `KIRBY_MCP_HTTP_SCOPES`                         | Comma-separated accepted operation scopes.                                                                                            |

## Troubleshooting

- “Unable to determine Kirby project root”: run from the Kirby project root or pass `--project=/absolute/path` (or set `KIRBY_MCP_PROJECT_ROOT`).
- Runtime-only tools fail: run `vendor/bin/kirby-mcp install` and check `kirby_runtime_status`.
- CLI command blocked: add patterns to `.kirby-mcp/mcp.json` (`cli.allow` / `cli.allowWrite`) or block with `cli.deny`.
- Runtime CLI commands cannot resolve PHP behind PHP-FPM/FrankenPHP: install an executable `PHP_BINDIR/php` or set `KIRBY_MCP_PHP_BINARY` to the PHP CLI binary path, e.g. `/opt/php-x.x/bin/php`.
- HTTP SSE closes before `sseMaxSeconds`: ensure `set_time_limit()` is enabled or set the host PHP/FrankenPHP `max_execution_time` above the configured stream lifetime.
- Host-specific config not applied: set `KIRBY_MCP_HOST`/`KIRBY_HOST` or configure `{"kirby":{"host":"..."}}`.
- Docs resources are slow/failing: confirm network access or adjust `docs.ttlSeconds` (set to `0` to disable caching).
- No dump output: ensure `dumps.enabled=true`, a `.kirby-mcp/dumps.jsonl` exists, and use the correct `traceId` with `kirby_dump_log_tail`.
- HTTP client gets 401/403: confirm Bearer auth, token audience/resource, scopes, and `Origin` match the configured HTTP settings.
- Claude custom connector cannot connect: confirm the public URL is the MCP endpoint (`https://example.com/mcp`), the route helper is registered, `http.enabled=true`, `http.auth.mode=oauth`, `http.oauthProvider.enabled=true`, and non-loopback requests reach Kirby over HTTPS.

## Development

- Install deps: `composer install`
- Run tests: `composer test`
- Run static analysis: `composer analyse`

## Disclaimer

This MCP server is provided "as is" with no guarantee. Use it at your own risk and always test it yourself before using it
in a production environment. If you find any issues,
please [create a new issue](https://github.com/bnomei/kirby-mcp/issues/new).

## License

[MIT](https://opensource.org/licenses/MIT)

It is discouraged to use this MCP server in any project that promotes racism, sexism, homophobia, animal abuse, violence or
any other form of hate speech.