fix(mcp): show commands and configuration to read:sensitive tokens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-28 14:16:22 +01:00
co-authored by Claude Opus 5.5
parent afe8fc05ea
commit e7a46bd790
4 changed files with 84 additions and 4 deletions
+14
View File
@@ -80,6 +80,20 @@ trait ResolvesTeam
return $this->mcpError($request, "{$action} requires confirm=true.");
}
/**
* Same rule as the REST API (ApiSensitiveData): a root or read:sensitive token of a team admin or owner.
*/
protected function canReadSensitiveData(Request $request): bool
{
$user = $request->user();
$token = $user?->currentAccessToken();
if (! $token || ! ($token->can('root') || $token->can('read:sensitive'))) {
return false;
}
return $token->team_id !== null && $user->isAdminOfTeam((int) $token->team_id);
}
protected function resolveTeamId(Request $request): ?int
{
$user = $request->user();
+5 -2
View File
@@ -16,7 +16,7 @@ class GetApplication extends Tool
{
protected string $name = 'get_application';
protected string $description = 'Get full details for a single application by UUID.';
protected string $description = 'Get full details for a single application by UUID. Commands and custom configuration (build, start, deployment, and healthcheck commands, docker run options, nginx configuration) need read:sensitive.';
use BuildsResponse;
use ResolvesTeam;
@@ -47,8 +47,11 @@ class GetApplication extends Tool
$application->setRelations([]);
$application->makeHidden(['destination', 'source', 'additional_servers', 'environment', 'tags', 'environmentVariables']);
$visibleSensitiveKeys = $this->canReadSensitiveData($request) ? Application::COMMAND_AND_CONFIGURATION_ATTRIBUTES : [];
$application->makeVisible($visibleSensitiveKeys);
return $this->mcpSuccess($request, $this->respond(
$this->scrubSensitive($application->toArray()),
$this->scrubSensitive($application->toArray(), $visibleSensitiveKeys),
$this->actionsForApplication($uuid, $application->status),
), ['resource_uuid' => $uuid]);
}
+5 -2
View File
@@ -15,7 +15,7 @@ class GetDatabase extends Tool
{
protected string $name = 'get_database';
protected string $description = 'Get full details for a standalone database by UUID. Detects type across postgresql, mysql, mariadb, mongodb, redis, keydb, dragonfly, clickhouse, sqlite.';
protected string $description = 'Get full details for a standalone database by UUID. Detects type across postgresql, mysql, mariadb, mongodb, redis, keydb, dragonfly, clickhouse, sqlite. Custom configuration (database config file, docker run options) needs read:sensitive.';
use BuildsResponse;
use ResolvesTeam;
@@ -45,8 +45,11 @@ class GetDatabase extends Tool
$database->setRelations([]);
$database->makeHidden(['destination', 'source', 'environment', 'environment_variables', 'environment_variables_preview']);
$visibleSensitiveKeys = $this->canReadSensitiveData($request) ? $database::CONFIGURATION_ATTRIBUTES : [];
$database->makeVisible($visibleSensitiveKeys);
return $this->mcpSuccess($request, $this->respond(
$this->scrubSensitive($database->toArray()),
$this->scrubSensitive($database->toArray(), $visibleSensitiveKeys),
$this->actionsForDatabase($uuid, $database->status ?? null),
), ['resource_uuid' => $uuid]);
}
+60
View File
@@ -644,6 +644,66 @@ test('get_application omits free-form configuration that can contain secrets', f
->and($raw)->not->toContain('custom_nginx_configuration');
});
function mcpReadCallWithAbilities(array $abilities, string $name, array $arguments = [])
{
auth()->forgetGuards();
return test()->withHeaders([
'Content-Type' => 'application/json',
'Accept' => 'application/json, text/event-stream',
'Authorization' => 'Bearer '.test()->user->createToken('mcp-abilities', $abilities)->plainTextToken,
])->postJson('/mcp', [
'jsonrpc' => '2.0',
'id' => 1,
'method' => 'tools/call',
'params' => ['name' => $name, 'arguments' => (object) $arguments],
]);
}
test('get_application returns commands and configuration to a read:sensitive or root token of an owner', function (array $abilities) {
$this->application->update([
'build_command' => 'npm run build',
'post_deployment_command' => 'php artisan migrate',
'custom_docker_run_options' => '--cap-add SYS_ADMIN',
]);
$response = mcpReadCallWithAbilities($abilities, 'get_application', ['uuid' => $this->application->uuid]);
$response->assertOk();
expect(mcpReadJson($response)['data'])
->toMatchArray([
'build_command' => 'npm run build',
'post_deployment_command' => 'php artisan migrate',
'custom_docker_run_options' => '--cap-add SYS_ADMIN',
])
->not->toHaveKey('git_full_url');
})->with([
'read:sensitive' => [['read', 'read:sensitive']],
'root' => [['root']],
]);
test('get_database returns custom configuration to a read:sensitive token of an owner', function () {
$database = StandalonePostgresql::create([
'name' => 'Configured database',
'postgres_password' => 'database-password',
'postgres_conf' => 'max_connections = 200',
'custom_docker_run_options' => '--shm-size 1g',
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => $this->destination->getMorphClass(),
]);
$response = mcpSensitiveReadCall('get_database', ['uuid' => $database->uuid]);
$response->assertOk();
expect(mcpReadJson($response)['data'])
->toMatchArray([
'postgres_conf' => 'max_connections = 200',
'custom_docker_run_options' => '--shm-size 1g',
])
->not->toHaveKey('postgres_password');
});
test('get_database omits configuration blobs that can contain secrets', function () {
$database = StandalonePostgresql::create([
'name' => 'Secret-bearing config',