HTTP reference

Markdown

MCP server

From externa-core 1.4.0 (#121), Externa ships an optional first-party Model Context Protocol server at /mcp (streamable HTTP, laravel/mcp). AI clients such as Claude Code, Cursor, or VS Code can read collections, items, and files, and propose writes, with the same permissions as the in-app assistant. It is off by default.

Related

Shared tool gates and the approval inbox: AI tool approvals & audit. In-app agent: AI assistant model. Key scopes: API abuse controls & scopes. Env keys: AI configuration.

Not the docs MCP

This page is about the MCP server inside your Externa install. To give coding agents up-to-date Externa documentation, see Using with AI coding tools (Context7, llms.txt).

Enable

EnvDefaultEffect
AI_MCP_ENABLEDfalseTurns the endpoint on. When false, every request to /mcp returns 404 (EnsureMcpEnabledMiddleware).
AI_MCP_WRITE_ENABLEDfalseWhen false, write tools are not listed and refuse calls, even if AI_WRITE_TOOLS is on.
AI_MCP_RATE_LIMIT_PER_MINUTE60Dedicated mcp rate limiter. Keyed per API key (the key's own rate_limit_per_minute wins when set), else per user, else IP.
AI_MCP_ENABLED=true
AI_MCP_WRITE_ENABLED=false
AI_MCP_RATE_LIMIT_PER_MINUTE=60

Run php artisan optimize:clear (or externa:upgrade) after changing these keys when config is cached. The endpoint is CSRF-exempt (mcp, mcp/* in bootstrap/app.php).

Authentication

Clients authenticate with an owned API key sent as a Bearer token. OAuth / Passport is not wired yet.

POST /mcp
Authorization: Bearer ek_YOUR_SECRET

AuthenticateMcpRequestMiddleware rejects the request when:

CheckResponse
Unknown, revoked, or expired key401
Key has no owner (owner_id null)401
Owner is deactivated or deleted401
Request IP not in the key's IP allowlist401
Key role is super-admin401
Key lacks the mcp:read scope403
No Bearer token and no authenticated user on the call401

After these checks the request runs as the key owner, with the key's role and scopes as an extra ceiling. Each call updates the key's last_used_at.

API key scopes and owners

API keys gain two fields in 1.4.0:

FieldValuesNotes
scopesmcp:read, mcp:writeNamed capability scopes (ApiKeyScopeEnum). null = none, so existing keys cannot call MCP until you grant mcp:read.
owner_iduser idEffective access = key role ∩ key scopes (collection_slugs, read_only, files_scope) ∩ owner's permissions (#215). Required for MCP.

Create or edit keys under Settings → API Keys (/settings/api-keys); the form picks the owner and the scopes. Keys are revoked automatically when their owner is deactivated or deleted. The upgrade backfills owners for existing keys — see Upgrade — 1.3.x → 1.4.0.

Write tools need all of: AI_MCP_WRITE_ENABLED=true, the mcp:write scope, and a key that is not read_only. Otherwise they are hidden from tools/list and refuse calls.

Tools

Tools are thin adapters over the shared AI tool registry (app/Mcp/Tools/*), so they run the same permission checks as the in-app assistant and write the same audit trail (surface=mcp).

Read tools

ToolArgumentsBacked by
list_collectionstrashed (optional)ManageCollections list
describe_collectioncollection_idManageCollections get (fields, types)
list_itemscollection_id, filter_json, limit (1–100)QueryCollectionItems
get_itemitem_idManageCollectionItems get
searchcollection_id, query, limitSearchSimilarCollectionItems
list_filesparent_id, query, limit (1–100)ManageFiles list (metadata only)
get_filefile_idFile metadata, no binary (can-show-files)

Write tools

Listed only when the write gate above allows it. There are no delete tools.

ToolArgumentsBacked by
create_itemcollection_id, data_jsonManageCollectionItems create
update_itemitem_id, data_json (merge)ManageCollectionItems update
publish_itemitem_idManageCollectionItems publish
move_filefile_id, target_parent_idManageFiles move
tag_filefile_id, tags_jsonManageFiles tag (replaces tags)

Write tools go through AiToolExecutorService, which also requires the AI write gate: AI_WRITE_TOOLS=true and the tool class (ManageCollectionItems or ManageFiles) on the write allowlist. Every mutating MCP call is then parked for human approval (pending_approval) instead of running. See AI tool approvals & audit.

publish_item also applies the collection update permission, the role's item filter, and the versioning checks of the admin publish action. Publishing through MCP only fires deploy hooks (Project settings) after the call has been approved (#435).

Resources

ResourceURIContent
collectionsexterna://collectionsCollections visible to the caller
collection_schemaexterna://collections/{slug}/schemaField schema of one collection
collection_itemexterna://items/{collection}/{id}One item (same checks as get_item)
file_metadataexterna://files/{id}File metadata, no binary (same as get_file)

Authorization

From #435, every MCP tool and resource enforces the same rules as the chat tools:

  • collection, item, and field permissions (including the role's item filter and field-level read rules);
  • file ACL and read_private;
  • the API key's collection_slugs, files_scope, and read_only.

A key scoped to collection_slugs=[blog] cannot read other collections over MCP, and a key with files_scope=none cannot read files. Prompt-injection and denied-permission cases for chat and MCP are covered by the Pest suite under tests/Feature/Ai/Security/* (#204).

Client setup

Any MCP client that supports streamable HTTP with a custom header works. Examples (replace the host and key):

# Claude Code
claude mcp add --header "Authorization: Bearer ek_YOUR_SECRET" \
  --transport http externa https://cms.example.com/mcp
{
  "mcpServers": {
    "externa": {
      "url": "https://cms.example.com/mcp",
      "headers": { "Authorization": "Bearer ek_YOUR_SECRET" }
    }
  }
}

Least privilege

Give MCP keys a dedicated non-admin role, an owner who should actually see that content, collection_slugs / files_scope set to what the agent needs, and only mcp:read unless you plan to approve writes. Treat the key like a password.

Source map

ConcernLocation (externa-core)
Routeroutes/mcp.php (Mcp::web('/mcp', ExternaServer::class))
Serverapp/Mcp/Servers/ExternaServer.php
Toolsapp/Mcp/Tools/*, app/Mcp/Tools/Concerns/DelegatesToRegistryMcpToolTrait.php
Resourcesapp/Mcp/Resources/*
Authapp/Http/Middleware/AuthenticateMcpRequestMiddleware.php, EnsureMcpEnabledMiddleware.php
Rate limiterAppServiceProvider (RateLimiter::for('mcp', …))
Configconfig/ai.php → mcp.*
Teststests/Feature/Mcp/*, tests/Feature/Ai/Security/McpAuthorizationParityTest.php