Search by

MCP server integration for Orbis CMS.

Package info

gitlab.com/orbis-cms/mcp

Issues

pkg:composer/orbis-cms/mcp

Statistics

Installs: 24

Dependents: 0

Suggesters: 0

Stars: 0

0.0.10 2026-09-28 23:36 UTC

This package is auto-updated.

Last update: 2026-09-28 20:38:37 UTC


README

MCP transport for Orbis CMS, built on Laravel MCP. It does not introduce an authoring layer: content creation and updates continue to use Core's ContentCreator.

Requirements

  • PHP 8.3 or higher
  • Laravel 13
  • orbis-cms/core 0.0
  • laravel/mcp 1.0
  • laravel/passport 13.0
  • orchid/platform 14.0 (already required by orbis-cms/core)

Installation

composer require orbis-cms/mcp
php artisan migrate

Register the provided server in routes/ai.php:

use Laravel\Mcp\Facades\Mcp;
use OrbisCms\Mcp\Servers\OrbisCmsServer;

Mcp::web('/mcp/orbis', OrbisCmsServer::class)->middleware('mcp.auth');

Authentication

The package owns MCP authentication end to end; the host application does not configure a guard itself. On boot, McpServiceProvider:

  • registers a dedicated mcp guard (Passport's token guard, kept separate from the application's own api guard so MCP scoping never leaks into it);
  • publishes the OAuth 2.0 discovery, dynamic client registration, and authorization endpoints (Mcp::oauthRoutes()) that Claude, ChatGPT, and other MCP hosts use to connect as a remote connector;
  • defines an mcp.auth middleware group that requires a valid mcp guard token carrying the mcp:use scope.

Apply ->middleware('mcp.auth') to the server route, as shown above, and nothing else. There is no per-content-type or per-action authorization: any principal holding a token with the mcp:use scope has full access to every tool and resource registered on the server.

The Eloquent model backing the mcp guard's provider (auth.providers.<name>.model) must use Passport's Laravel\Passport\HasApiTokens trait. Without it, Passport's token guard throws a BadMethodCallException from withAccessToken() on every authenticated request — this is a host-side model requirement, not something the package can enforce or recover from.

mcp.auth always returns a bare 401/403 JSON response on failure, regardless of how the host application configures exception rendering. Laravel's exception handler resolves the JSON-vs-redirect decision for AuthenticationException through two app-wide, guard-unaware hooks — Handler::shouldRenderJsonWhen() and AuthenticationException::redirectUsing() (set via bootstrap/app.php's ->shouldRenderJsonWhen()/->redirectGuestsTo()) — and either one, configured by the host for its own unrelated routes, would otherwise silently turn an MCP 401 into a 302 redirect to an HTML login page, breaking the WWW-Authenticate discovery handshake MCP clients rely on to start OAuth. EnsureMcpFailuresAreJson catches auth failures before they reach that global handler, so this holds no matter what the host configures.

The MCP agent and the "Authorize agent" flow

Every access token is issued to one dedicated MCP agent — a regular application user record, resolved by McpAgentResolver. No human ever logs in as it. Two ways to identify it, configured under orbis-mcp.agent:

  • user_id (default null) — point at an existing user explicitly; that record must already exist and is used as-is, so a misconfigured ID fails loudly instead of silently creating a duplicate.
  • email (default null, falling back to mcp-agent@<app host>) — used only when user_id is null: the user is found by that email or created automatically on first use if it does not exist yet.

An Orchid operator authorizes a connector (ChatGPT, Claude, ...) on the agent's behalf instead. McpServiceProvider replaces Passport's own GET /oauth/authorize step with McpAuthorizeController, which:

  1. requires the visitor to hold the orbis-cms-mcp.authorize Orchid permission (grant it to whichever roles should be allowed to connect agents, from the standard Orchid roles screen);
  2. shows a single "Authorize agent" / "Deny" confirmation naming the requesting client, with no per-scope grant step;
  3. on approval, issues the token to the MCP agent, not to the operator who clicked the button.

Passport's stock /oauth/authorize POST (approve) and DELETE (deny) endpoints, and the /oauth/token exchange, are untouched — only the initial human-facing consent step is replaced.

Direct access tokens

For integrations that can send a configurable Authorization header, Orchid offers MCP access tokens at /admin/mcp/tokens. Operators with the orbis-cms-mcp.authorize permission can create and revoke them. A token is issued to the dedicated MCP agent with only the mcp:use scope, shown after creation, and used as Authorization: Bearer <token>.

The first direct token automatically creates Passport's personal access client for the MCP user provider if it does not already exist. The token screen only lists and can revoke these personal access tokens; OAuth connections continue to be managed through their own flow and cannot be affected there.

Content definitions

An application Contentable model is exposed only when it implements HasMcpDefinition. Its fields live beside the model and use the model attribute name directly. A field is ReadOnly by default; set FieldAccess::ReadWrite explicitly for data that MCP may accept. A field omitted from the definition is not exposed to MCP.

use OrbisCms\Mcp\Contracts\HasMcpDefinition;
use OrbisCms\Mcp\Definitions\FieldDefinition;
use OrbisCms\Mcp\Definitions\FieldAccess;
use OrbisCms\Core\Models\Contracts\Contentable;
use Illuminate\JsonSchema\JsonSchema;

class Article extends Model implements Contentable, HasMcpDefinition
{
    public static function mcpDescription(): string
    {
        return 'Editorial article.';
    }

    public static function mcpFields(): array
    {
        return [
            new FieldDefinition(
                name: 'title',
                schema: JsonSchema::string()
                    ->title('Article title')
                    ->description('Article title.')
                    ->max(255)
                    ->required(),
                example: 'Introducing MCP',
                access: FieldAccess::ReadWrite,
            ),
            new FieldDefinition(
                name: 'body',
                schema: JsonSchema::string()->description('Article body in Markdown.'),
                access: FieldAccess::ReadWrite,
            ),
        ];
    }
}

SchemaAdapter builds one schema per content type covering both what a caller may write and what it reads back, instead of separate input/output schemas: ReadOnly fields carry readOnly: true and are excluded from required (which therefore means "required to write"). A Pageable content type additionally includes the related Core Page as a page object in that same schema; its URI and publication status are read-only. Content types that are only Contentable work without a Page.

The mutable input and returned content share one shape: model data is nested under attributes, and a Pageable type may additionally carry a page relation. This avoids collisions between model and Page fields such as title.

Laravel validation on creationRules() and updateRules() remains authoritative. Before a call reaches ContentCreator, this package rejects attributes not present in the definition or marked read-only.

MCP API

Resources:

  • orbis://content-types
  • orbis://content-types/{alias}/schema

Tools:

  • orbis.list_content_types
  • orbis.describe_content_type (full schema for a type alias)
  • orbis.attach_image (create an image from Base64 data and add it to an image-capable content record)
  • orbis.search_pages
  • orbis.list_content
  • orbis.get_content (by type alias and content ID)
  • orbis.get_page (by Page ID or canonical URI)
  • orbis.create_content
  • orbis.update_content

orbis.create_content creates a Page only when page data is supplied for a Pageable type; that Page is always a draft. Neither create nor update accepts a publication status. Draft mutations return a signed preview URL; publication remains a human CMS operation.

orbis.list_content omits a string attribute from a record's attributes when it is longer than orbis-mcp.list.hidden_length_threshold characters (default 200, configurable via ORBIS_MCP_LIST_HIDDEN_LENGTH), naming it instead in that record's hiddenAttributes — this keeps a page of results compact even when a content type has a long HTML/Markdown body. orbis.get_content always returns the full value.

orbis.update_content uses PATCH semantics: supply only the writable content attributes or Page metadata that should change. Only supplied fields are validated and updated; all other fields retain their existing values. When page data creates a previously absent Page, its normal creation validation still applies.

Extending the server

The McpRegistry is independent of Core's ContentRegistry. The package service provider registers the standard CMS primitives in it. Add application-specific primitives to that same registry in routes/ai.php:

use OrbisCms\Mcp\Facades\OrbisMcp;

OrbisMcp::tool(SyncCatalogTool::class);
OrbisMcp::resource(ProductGuideResource::class);

The registry accepts only subclasses of Laravel MCP's Tool and Resource, ignores duplicate registrations, and is read by OrbisCmsServer during MCP server boot. This keeps application tools on the same endpoint and behind the same middleware as standard CMS tools.

Development

The test suite has two layers:

  • tests/Unit for isolated registry, definition, schema, and transformation tests;
  • tests/Feature for Laravel container, database, MCP transport, and Orbis CMS integration tests.

Run tests and style checks with:

composer test
composer pint:test

License

This package is open-sourced software licensed under the MIT license.