'applications/{uuid}/envs']). * * @return array, ability: string, confirm?: bool, description: string}> */ abstract protected function actions(): array; /** * Path parameters this tool accepts, with descriptions. * * @return array */ abstract protected function pathParams(): array; /** * Description of the fields accepted in `data`. Empty when the tool takes no body. */ protected function dataDescription(): string { return ''; } /** * Actions that the current token has the ability for. * * @return array, ability: string, confirm?: bool, description: string}> */ protected function availableActions(): array { return array_filter($this->actions(), fn (array $action): bool => $this->currentTokenCan($action['ability'])); } /** * Names of the actions that the current token has the ability for. * * @return array */ public function availableActionNames(): array { return array_keys($this->availableActions()); } public function shouldRegister(): bool { return $this->availableActions() !== []; } public function handle(Request $request): Response { $actions = $this->availableActions(); $actionName = $request->get('action'); $allActions = $this->actions(); if (! is_string($actionName) || ! isset($allActions[$actionName])) { return $this->mcpError($request, 'action must be one of: '.implode(', ', array_keys($actions)).'.'); } $action = $allActions[$actionName]; // Hidden actions still reach this check, so the agent gets the missing ability, not "unknown action". if ($error = $this->ensureAbility($request, $action['ability'], $this->name)) { return $error; } if (is_null($this->resolveTeamId($request))) { return $this->mcpError($request, 'Invalid token.'); } if (($action['confirm'] ?? false) && $error = $this->ensureConfirmed($request, "{$this->name} {$actionName}")) { return $error; } $path = $action['path'] ?? null; if (isset($action['paths'])) { $resource = $request->get('resource'); if (! is_string($resource) || ! isset($action['paths'][$resource])) { return $this->mcpError($request, 'resource must be one of: '.implode(', ', array_keys($action['paths'])).'.'); } $path = $action['paths'][$resource]; } $data = $request->get('data') ?? []; if (! is_array($data) || ($data !== [] && array_is_list($data))) { return $this->mcpError($request, 'data must be a JSON object.'); } $pathParams = []; foreach (array_keys($this->pathParams()) as $param) { $value = $request->get($param); if ($value !== null) { if (! is_scalar($value)) { return $this->mcpError($request, "{$param} must be a string."); } $pathParams[$param] = $value; } } try { $result = $this->callRestApi($action['method'], $path, $pathParams, $data); } catch (\InvalidArgumentException $e) { return $this->mcpError($request, $e->getMessage()); } $context = ['action' => $actionName, 'resource' => $request->get('resource'), 'status' => $result['status']] + array_map('strval', $pathParams); if ($result['status'] >= 400) { $body = is_array($result['body']) ? $result['body'] : []; $message = (string) ($body['message'] ?? 'Request failed.'); if (isset($body['errors']) && is_array($body['errors'])) { $message .= ' '.json_encode($body['errors']); } if (isset($body['conflict']) && is_array($body['conflict'])) { $message .= ' '.json_encode(['conflict' => $body['conflict']]); } return $this->mcpError($request, $message, $context); } return $this->mcpSuccess($request, $this->respond(is_array($result['body']) ? $result['body'] : []), $context); } public function schema(JsonSchema $schema): array { $actionLines = collect($this->availableActions()) ->map(fn (array $action, string $name): string => "{$name}: {$action['description']}".(($action['confirm'] ?? false) ? ' (needs confirm=true)' : '')) ->implode('; '); $fields = [ 'action' => $schema->string()->enum(array_keys($this->availableActions()))->description($actionLines)->required(), ]; $resources = collect($this->availableActions())->pluck('paths')->filter()->flatMap(fn (array $paths): array => array_keys($paths))->unique()->values()->all(); if ($resources !== []) { $fields['resource'] = $schema->string()->enum($resources)->description('Resource type for actions that support several ('.implode(' | ', $resources).').'); } foreach ($this->pathParams() as $param => $description) { $fields[$param] = $schema->string()->description($description); } if ($this->dataDescription() !== '') { $fields['data'] = $schema->object()->description($this->dataDescription()); } if (collect($this->availableActions())->contains(fn (array $action): bool => $action['confirm'] ?? false)) { $fields['confirm'] = $schema->boolean()->description('Must be true for destructive actions.'); } return $fields; } }