Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@
"filament/filament": "^5.0",
"filament/spatie-laravel-settings-plugin": "^5.0",
"guzzlehttp/guzzle": "^7.8",
"illuminate/cache": "^11.35.0|^12.0|^13.0",
"illuminate/console": "^11.35.0|^12.0|^13.0",
"illuminate/database": "^11.35.0|^12.0|^13.0",
"illuminate/events": "^11.35.0|^12.0|^13.0",
"illuminate/queue": "^11.35.0|^12.0|^13.0",
"illuminate/support": "^11.35.0|^12.0|^13.0",
"illuminate/cache": "^11.45.3|^12.41.1|^13.0",
"illuminate/console": "^11.45.3|^12.41.1|^13.0",
"illuminate/database": "^11.45.3|^12.41.1|^13.0",
"illuminate/events": "^11.45.3|^12.41.1|^13.0",
"illuminate/queue": "^11.45.3|^12.41.1|^13.0",
"illuminate/support": "^11.45.3|^12.41.1|^13.0",
"laravel/mcp": "^0.8",
"laravel/sanctum": "^4.0",
"nesbot/carbon": "^2.70|^3.0",
"spatie/laravel-data": "^4.11",
Expand All @@ -44,7 +45,6 @@
"dedoc/scramble": "^0.12|^0.13",
"larastan/larastan": "^3.4",
"laravel/boost": "^2.0",
"laravel/mcp": "^0.5",
"laravel/pail": "^1.1",
"laravel/pint": "^1.24",
"orchestra/testbench": "^9.15.1|^10.0|^11.0",
Expand Down
29 changes: 29 additions & 0 deletions config/cachet.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

use App\Models\User;
use Cachet\Http\Middleware\AuthenticateApiIfProtected;
use Cachet\Http\Middleware\AuthenticateMcpIfProtected;
use Cachet\Http\Middleware\EnsureApiIsEnabled;
use Cachet\Http\Middleware\EnsureMcpIsEnabled;
use Cachet\Http\Middleware\ForceJsonResponse;
use Illuminate\Routing\Middleware\SubstituteBindings;

Expand Down Expand Up @@ -98,6 +100,21 @@
SubstituteBindings::class,
],

/*
|--------------------------------------------------------------------------
| Cachet MCP Middleware
|--------------------------------------------------------------------------
|
| This is the middleware that will be applied to the Cachet MCP server
| endpoint. The MCP server is disabled until the "Enable MCP server"
| setting is turned on from the dashboard.
|
*/
'mcp_middleware' => [
EnsureMcpIsEnabled::class,
AuthenticateMcpIfProtected::class,
],

'trusted_proxies' => env('CACHET_TRUSTED_PROXIES', ''),

/*
Expand All @@ -112,6 +129,18 @@
*/
'api_rate_limit' => env('CACHET_API_RATE_LIMIT', 300),

/*
|--------------------------------------------------------------------------
| Cachet MCP Rate Limit (attempts per minute)
|--------------------------------------------------------------------------
|
| This is the rate limit for the Cachet MCP server. By default, the MCP
| server is rate limited to 300 requests a minute. You can adjust the
| limit as needed by your application.
|
*/
'mcp_rate_limit' => env('CACHET_MCP_RATE_LIMIT', 300),

/*
|--------------------------------------------------------------------------
| Cachet Beacon
Expand Down
15 changes: 15 additions & 0 deletions database/migrations/2026_07_13_000001_add_mcp_settings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

use Spatie\LaravelSettings\Migrations\SettingsMigration;

return new class extends SettingsMigration
{
/**
* Run the migrations.
*/
public function up(): void
{
rescue(fn () => $this->migrator->add('app.mcp_enabled', false));
rescue(fn () => $this->migrator->add('app.mcp_protected', true));
}
};
2 changes: 2 additions & 0 deletions database/seeders/DatabaseSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ public function run(): void
$appSettings->recent_incidents_days = 7;
$appSettings->api_enabled = true;
$appSettings->api_protected = false;
$appSettings->mcp_enabled = true;
$appSettings->mcp_protected = false;
$appSettings->save();

$customizationSettings = app(CustomizationSettings::class);
Expand Down
5 changes: 5 additions & 0 deletions resources/lang/en/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@
'api_enabled_helper' => 'Allow access to the Cachet API. When disabled, all API requests return a 404.',
'api_protected' => 'Require authentication',
'api_protected_helper' => 'Require an API token for all API requests, including read-only endpoints.',
'mcp_enabled' => 'Enable MCP server',
'mcp_enabled_helper' => 'Allow AI agents to connect to Cachet over the Model Context Protocol. When disabled, all MCP requests return a 404.',
'mcp_protected' => 'Require authentication',
'mcp_protected_helper' => 'Require an API token for all MCP connections. When off, read-only tools are public and write tools still require an API token with the matching ability.',
'dynamic_favicon' => 'Dynamic favicon',
'dynamic_favicon_helper' => 'Update the favicon to reflect the current status of your systems.',
],
'display_settings_title' => 'Display settings',
'api_settings_title' => 'API settings',
'mcp_settings_title' => 'MCP server settings',
],
'manage_customization' => [
'header_label' => 'Custom header HTML',
Expand Down
19 changes: 19 additions & 0 deletions src/CachetCoreServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Cachet\Database\Seeders\DemoMetricSeeder;
use Cachet\Listeners\SendWebhookListener;
use Cachet\Listeners\WebhookCallEventListener;
use Cachet\Mcp\CachetServer;
use Cachet\Models\Component;
use Cachet\Models\ComponentCheck;
use Cachet\Models\ComponentGroup;
Expand Down Expand Up @@ -43,6 +44,7 @@
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Laravel\Mcp\Facades\Mcp;
use Spatie\WebhookServer\Events\WebhookCallFailedEvent;
use Spatie\WebhookServer\Events\WebhookCallSucceededEvent;

Expand Down Expand Up @@ -88,6 +90,7 @@ public function boot(): void

Route::middlewareGroup('cachet', config('cachet.middleware', []));
Route::middlewareGroup('cachet:api', config('cachet.api_middleware', []));
Route::middlewareGroup('cachet:mcp', config('cachet.mcp_middleware', []));

Relation::morphMap([
'component' => Component::class,
Expand Down Expand Up @@ -178,6 +181,11 @@ private function configureRateLimiting(): void
return Limit::perMinute(config('cachet.api_rate_limit', 300))
->by($request->user()?->id ?: $request->ip());
});

RateLimiter::for('cachet-mcp', function ($request) {
return Limit::perMinute(config('cachet.mcp_rate_limit', 300))
->by($request->user('sanctum')?->getKey() ?: $request->ip());
});
}

/**
Expand All @@ -195,6 +203,17 @@ private function registerRoutes(): void
$this->loadRoutesFrom(__DIR__.'/../routes/api.php');
});

if ($application->runningInConsole() || ! $application->routesAreCached()) {
$router->group([
'domain' => config('cachet.domain'),
'as' => 'cachet.mcp.',
'prefix' => Cachet::path().'/mcp',
'middleware' => ['cachet:mcp', 'throttle:cachet-mcp'],
], function () {
Mcp::web('/', CachetServer::class)->name('server');
});
}

Cachet::routes()
->register();
});
Expand Down
14 changes: 14 additions & 0 deletions src/Filament/Pages/Settings/ManageCachet.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,20 @@ public function form(Schema $schema): Schema
->helperText(__('cachet::settings.manage_cachet.toggles.api_protected_helper'))
->visible(fn (Get $get) => $get('api_enabled') === true),
]),

Section::make(__('cachet::settings.manage_cachet.mcp_settings_title'))
->columns(2)
->schema([
Toggle::make('mcp_enabled')
->label(__('cachet::settings.manage_cachet.toggles.mcp_enabled'))
->helperText(__('cachet::settings.manage_cachet.toggles.mcp_enabled_helper'))
->afterStateUpdated(fn (Set $set) => $set('mcp_protected', true))
->reactive(),
Toggle::make('mcp_protected')
->label(__('cachet::settings.manage_cachet.toggles.mcp_protected'))
->helperText(__('cachet::settings.manage_cachet.toggles.mcp_protected_helper'))
->visible(fn (Get $get) => $get('mcp_enabled') === true),
]),
]);
}
}
31 changes: 31 additions & 0 deletions src/Http/Middleware/AuthenticateMcpIfProtected.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Cachet\Http\Middleware;

use Cachet\Settings\AppSettings;
use Closure;
use Illuminate\Contracts\Auth\Factory as AuthFactory;
use Illuminate\Http\Request;

class AuthenticateMcpIfProtected
{
public function __construct(
private AppSettings $settings,
private AuthFactory $auth,
) {}

public function handle(Request $request, Closure $next)
{
$authenticated = $this->auth->guard('sanctum')->check();

if ($this->settings->mcp_protected && ! $authenticated) {
abort(401, 'Unauthenticated.');
}

if ($authenticated) {
$this->auth->shouldUse('sanctum');
}

return $next($request);
}
}
19 changes: 19 additions & 0 deletions src/Http/Middleware/EnsureMcpIsEnabled.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Cachet\Http\Middleware;

use Cachet\Settings\AppSettings;
use Closure;
use Illuminate\Http\Request;

class EnsureMcpIsEnabled
{
public function __construct(private AppSettings $settings) {}

public function handle(Request $request, Closure $next)
{
abort_unless($this->settings->mcp_enabled, 404);

return $next($request);
}
}
129 changes: 129 additions & 0 deletions src/Mcp/CachetServer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

namespace Cachet\Mcp;

use Cachet\Cachet;
use Cachet\Mcp\Tools\ComponentGroups\CreateComponentGroup;
use Cachet\Mcp\Tools\ComponentGroups\DeleteComponentGroup;
use Cachet\Mcp\Tools\ComponentGroups\ListComponentGroups;
use Cachet\Mcp\Tools\ComponentGroups\UpdateComponentGroup;
use Cachet\Mcp\Tools\Components\CreateComponent;
use Cachet\Mcp\Tools\Components\DeleteComponent;
use Cachet\Mcp\Tools\Components\GetComponent;
use Cachet\Mcp\Tools\Components\ListComponents;
use Cachet\Mcp\Tools\Components\UpdateComponent;
use Cachet\Mcp\Tools\Incidents\CreateIncident;
use Cachet\Mcp\Tools\Incidents\DeleteIncident;
use Cachet\Mcp\Tools\Incidents\GetIncident;
use Cachet\Mcp\Tools\Incidents\ListIncidents;
use Cachet\Mcp\Tools\Incidents\UpdateIncident;
use Cachet\Mcp\Tools\IncidentTemplates\CreateIncidentTemplate;
use Cachet\Mcp\Tools\IncidentTemplates\DeleteIncidentTemplate;
use Cachet\Mcp\Tools\IncidentTemplates\ListIncidentTemplates;
use Cachet\Mcp\Tools\IncidentTemplates\UpdateIncidentTemplate;
use Cachet\Mcp\Tools\IncidentUpdates\DeleteIncidentUpdate;
use Cachet\Mcp\Tools\IncidentUpdates\EditIncidentUpdate;
use Cachet\Mcp\Tools\IncidentUpdates\RecordIncidentUpdate;
use Cachet\Mcp\Tools\MetricPoints\AddMetricPoint;
use Cachet\Mcp\Tools\MetricPoints\DeleteMetricPoint;
use Cachet\Mcp\Tools\Metrics\CreateMetric;
use Cachet\Mcp\Tools\Metrics\DeleteMetric;
use Cachet\Mcp\Tools\Metrics\GetMetric;
use Cachet\Mcp\Tools\Metrics\ListMetrics;
use Cachet\Mcp\Tools\Metrics\UpdateMetric;
use Cachet\Mcp\Tools\Schedules\CreateSchedule;
use Cachet\Mcp\Tools\Schedules\DeleteSchedule;
use Cachet\Mcp\Tools\Schedules\GetSchedule;
use Cachet\Mcp\Tools\Schedules\ListSchedules;
use Cachet\Mcp\Tools\Schedules\UpdateSchedule;
use Cachet\Mcp\Tools\ScheduleUpdates\DeleteScheduleUpdate;
use Cachet\Mcp\Tools\ScheduleUpdates\EditScheduleUpdate;
use Cachet\Mcp\Tools\ScheduleUpdates\RecordScheduleUpdate;
use Cachet\Mcp\Tools\Status\GetStatus;
use Cachet\Mcp\Tools\Subscribers\CreateSubscriber;
use Cachet\Mcp\Tools\Subscribers\ListSubscribers;
use Cachet\Mcp\Tools\Subscribers\UnsubscribeSubscriber;
use Cachet\Mcp\Tools\Subscribers\UpdateSubscriber;
use Laravel\Mcp\Server;
use Laravel\Mcp\Server\Contracts\Transport;
use Laravel\Mcp\Server\Tool;

class CachetServer extends Server
{
protected string $name = 'Cachet';

protected string $instructions = <<<'MARKDOWN'
This server exposes the Cachet status page dashboard over the Model Context Protocol:
manage components, component groups, incidents and their updates, incident templates,
maintenance schedules and their updates, metrics and metric points, and subscribers.

Read tools are available to every session. Write tools appear only when the session is
authenticated with a Cachet API token holding the matching ability (for example
`incidents.manage` or `components.delete`). API tokens are created from the Cachet
dashboard under Settings, API Keys, and are sent as a bearer token.

Statuses are integers. Component status: 1 operational, 2 performance issues,
3 partial outage, 4 major outage, 5 unknown, 6 under maintenance. Incident status:
0 unknown, 1 investigating, 2 identified, 3 watching, 4 fixed.

Linking components to an incident overlays their displayed status rather than
changing the component itself: a component's `status` is its own base status, while
`latest_status` is what the status page shows, taking any unresolved incident into
account. Recording an incident update with status 4 (fixed) resolves the incident
and restores the displayed status of its affected components.
MARKDOWN;

/**
* @var array<int, class-string<Tool>>
*/
protected array $tools = [
GetStatus::class,
ListComponents::class,
GetComponent::class,
CreateComponent::class,
UpdateComponent::class,
DeleteComponent::class,
ListComponentGroups::class,
CreateComponentGroup::class,
UpdateComponentGroup::class,
DeleteComponentGroup::class,
ListIncidents::class,
GetIncident::class,
CreateIncident::class,
UpdateIncident::class,
DeleteIncident::class,
RecordIncidentUpdate::class,
EditIncidentUpdate::class,
DeleteIncidentUpdate::class,
ListIncidentTemplates::class,
CreateIncidentTemplate::class,
UpdateIncidentTemplate::class,
DeleteIncidentTemplate::class,
ListSchedules::class,
GetSchedule::class,
CreateSchedule::class,
UpdateSchedule::class,
DeleteSchedule::class,
RecordScheduleUpdate::class,
EditScheduleUpdate::class,
DeleteScheduleUpdate::class,
ListMetrics::class,
GetMetric::class,
CreateMetric::class,
UpdateMetric::class,
DeleteMetric::class,
AddMetricPoint::class,
DeleteMetricPoint::class,
ListSubscribers::class,
CreateSubscriber::class,
UpdateSubscriber::class,
UnsubscribeSubscriber::class,
];

public function __construct(Transport $transport)
{
parent::__construct($transport);

$this->version = Cachet::version();
}
}
Loading
Loading