feat(api): standardize agent-facing response envelopes
This commit is contained in:
parent
ca9b495884
commit
ee3fba1e7a
8 changed files with 182 additions and 117 deletions
|
|
@ -11,15 +11,33 @@ use Illuminate\Http\JsonResponse;
|
|||
*/
|
||||
trait HasApiResponses
|
||||
{
|
||||
/**
|
||||
* Return a standard error response.
|
||||
*/
|
||||
protected function errorResponse(
|
||||
string $errorCode,
|
||||
string $message,
|
||||
array $meta = [],
|
||||
int $status = 400,
|
||||
): JsonResponse {
|
||||
return response()->json(array_merge([
|
||||
'success' => false,
|
||||
'error' => $errorCode,
|
||||
'message' => $message,
|
||||
'error_code' => $errorCode,
|
||||
], $meta), $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a no workspace response.
|
||||
*/
|
||||
protected function noWorkspaceResponse(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'error' => 'no_workspace',
|
||||
'message' => 'No workspace found. Please select a workspace first.',
|
||||
], 404);
|
||||
return $this->errorResponse(
|
||||
errorCode: 'no_workspace',
|
||||
message: 'No workspace found. Please select a workspace first.',
|
||||
status: 404,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -27,10 +45,14 @@ trait HasApiResponses
|
|||
*/
|
||||
protected function notFoundResponse(string $resource = 'Resource'): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'error' => 'not_found',
|
||||
'message' => "{$resource} not found.",
|
||||
], 404);
|
||||
return $this->errorResponse(
|
||||
errorCode: 'not_found',
|
||||
message: "{$resource} not found.",
|
||||
meta: [
|
||||
'resource' => $resource,
|
||||
],
|
||||
status: 404,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -38,12 +60,15 @@ trait HasApiResponses
|
|||
*/
|
||||
protected function limitReachedResponse(string $feature, ?string $message = null): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'error' => 'feature_limit_reached',
|
||||
'message' => $message ?? 'You have reached your limit for this feature.',
|
||||
'feature' => $feature,
|
||||
'upgrade_url' => route('hub.usage'),
|
||||
], 403);
|
||||
return $this->errorResponse(
|
||||
errorCode: 'feature_limit_reached',
|
||||
message: $message ?? 'You have reached your limit for this feature.',
|
||||
meta: [
|
||||
'feature' => $feature,
|
||||
'upgrade_url' => route('hub.usage'),
|
||||
],
|
||||
status: 403,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -51,10 +76,20 @@ trait HasApiResponses
|
|||
*/
|
||||
protected function accessDeniedResponse(string $message = 'Access denied.'): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'error' => 'access_denied',
|
||||
'message' => $message,
|
||||
], 403);
|
||||
return $this->forbiddenResponse($message, status: 403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a forbidden response.
|
||||
*/
|
||||
protected function forbiddenResponse(string $message, array $meta = [], int $status = 403): JsonResponse
|
||||
{
|
||||
return $this->errorResponse(
|
||||
errorCode: 'forbidden',
|
||||
message: $message,
|
||||
meta: $meta,
|
||||
status: $status,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -63,6 +98,7 @@ trait HasApiResponses
|
|||
protected function successResponse(string $message, array $data = []): JsonResponse
|
||||
{
|
||||
return response()->json(array_merge([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
], $data));
|
||||
}
|
||||
|
|
@ -73,6 +109,7 @@ trait HasApiResponses
|
|||
protected function createdResponse(mixed $resource, string $message = 'Created successfully.'): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => $resource,
|
||||
], 201);
|
||||
|
|
@ -81,13 +118,16 @@ trait HasApiResponses
|
|||
/**
|
||||
* Return a validation error response.
|
||||
*/
|
||||
protected function validationErrorResponse(array $errors): JsonResponse
|
||||
protected function validationErrorResponse(array $errors, int $status = 422): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'error' => 'validation_failed',
|
||||
'message' => 'The given data was invalid.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
return $this->errorResponse(
|
||||
errorCode: 'validation_failed',
|
||||
message: 'The given data was invalid.',
|
||||
meta: [
|
||||
'errors' => $errors,
|
||||
],
|
||||
status: $status,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -97,10 +137,11 @@ trait HasApiResponses
|
|||
*/
|
||||
protected function invalidStatusResponse(string $message): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'error' => 'invalid_status',
|
||||
'message' => $message,
|
||||
], 422);
|
||||
return $this->errorResponse(
|
||||
errorCode: 'invalid_status',
|
||||
message: $message,
|
||||
status: 422,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -110,15 +151,13 @@ trait HasApiResponses
|
|||
*/
|
||||
protected function providerErrorResponse(string $message, ?string $provider = null): JsonResponse
|
||||
{
|
||||
$response = [
|
||||
'error' => 'provider_error',
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
if ($provider !== null) {
|
||||
$response['provider'] = $provider;
|
||||
}
|
||||
|
||||
return response()->json($response, 400);
|
||||
return $this->errorResponse(
|
||||
errorCode: 'provider_error',
|
||||
message: $message,
|
||||
meta: array_filter([
|
||||
'provider' => $provider,
|
||||
]),
|
||||
status: 400,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Api\Controllers\Api;
|
||||
|
||||
use Core\Api\Concerns\HasApiResponses;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
|
@ -16,6 +17,8 @@ use Core\Social\Models\Webhook;
|
|||
*/
|
||||
class WebhookSecretController extends Controller
|
||||
{
|
||||
use HasApiResponses;
|
||||
|
||||
public function __construct(
|
||||
protected WebhookSecretRotationService $rotationService
|
||||
) {}
|
||||
|
|
@ -28,7 +31,7 @@ class WebhookSecretController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$webhook = Webhook::where('workspace_id', $workspace->id)
|
||||
|
|
@ -36,7 +39,7 @@ class WebhookSecretController extends Controller
|
|||
->first();
|
||||
|
||||
if (! $webhook) {
|
||||
return response()->json(['error' => 'Webhook not found'], 404);
|
||||
return $this->notFoundResponse('Webhook');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
|
|
@ -66,7 +69,7 @@ class WebhookSecretController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$endpoint = ContentWebhookEndpoint::where('workspace_id', $workspace->id)
|
||||
|
|
@ -74,7 +77,7 @@ class WebhookSecretController extends Controller
|
|||
->first();
|
||||
|
||||
if (! $endpoint) {
|
||||
return response()->json(['error' => 'Webhook endpoint not found'], 404);
|
||||
return $this->notFoundResponse('Webhook endpoint');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
|
|
@ -104,7 +107,7 @@ class WebhookSecretController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$webhook = Webhook::where('workspace_id', $workspace->id)
|
||||
|
|
@ -112,7 +115,7 @@ class WebhookSecretController extends Controller
|
|||
->first();
|
||||
|
||||
if (! $webhook) {
|
||||
return response()->json(['error' => 'Webhook not found'], 404);
|
||||
return $this->notFoundResponse('Webhook');
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
|
|
@ -128,7 +131,7 @@ class WebhookSecretController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$endpoint = ContentWebhookEndpoint::where('workspace_id', $workspace->id)
|
||||
|
|
@ -136,7 +139,7 @@ class WebhookSecretController extends Controller
|
|||
->first();
|
||||
|
||||
if (! $endpoint) {
|
||||
return response()->json(['error' => 'Webhook endpoint not found'], 404);
|
||||
return $this->notFoundResponse('Webhook endpoint');
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
|
|
@ -152,7 +155,7 @@ class WebhookSecretController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$webhook = Webhook::where('workspace_id', $workspace->id)
|
||||
|
|
@ -160,7 +163,7 @@ class WebhookSecretController extends Controller
|
|||
->first();
|
||||
|
||||
if (! $webhook) {
|
||||
return response()->json(['error' => 'Webhook not found'], 404);
|
||||
return $this->notFoundResponse('Webhook');
|
||||
}
|
||||
|
||||
$this->rotationService->invalidatePreviousSecret($webhook);
|
||||
|
|
@ -179,7 +182,7 @@ class WebhookSecretController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$endpoint = ContentWebhookEndpoint::where('workspace_id', $workspace->id)
|
||||
|
|
@ -187,7 +190,7 @@ class WebhookSecretController extends Controller
|
|||
->first();
|
||||
|
||||
if (! $endpoint) {
|
||||
return response()->json(['error' => 'Webhook endpoint not found'], 404);
|
||||
return $this->notFoundResponse('Webhook endpoint');
|
||||
}
|
||||
|
||||
$this->rotationService->invalidatePreviousSecret($endpoint);
|
||||
|
|
@ -206,7 +209,7 @@ class WebhookSecretController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$webhook = Webhook::where('workspace_id', $workspace->id)
|
||||
|
|
@ -214,7 +217,7 @@ class WebhookSecretController extends Controller
|
|||
->first();
|
||||
|
||||
if (! $webhook) {
|
||||
return response()->json(['error' => 'Webhook not found'], 404);
|
||||
return $this->notFoundResponse('Webhook');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
|
|
@ -240,7 +243,7 @@ class WebhookSecretController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$endpoint = ContentWebhookEndpoint::where('workspace_id', $workspace->id)
|
||||
|
|
@ -248,7 +251,7 @@ class WebhookSecretController extends Controller
|
|||
->first();
|
||||
|
||||
if (! $endpoint) {
|
||||
return response()->json(['error' => 'Webhook endpoint not found'], 404);
|
||||
return $this->notFoundResponse('Webhook endpoint');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Api\Controllers\Api;
|
||||
|
||||
use Core\Api\Concerns\HasApiResponses;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
|
@ -17,6 +18,8 @@ use Core\Api\Services\WebhookTemplateService;
|
|||
*/
|
||||
class WebhookTemplateController extends Controller
|
||||
{
|
||||
use HasApiResponses;
|
||||
|
||||
public function __construct(
|
||||
protected WebhookTemplateService $templateService
|
||||
) {}
|
||||
|
|
@ -29,7 +32,7 @@ class WebhookTemplateController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$query = WebhookPayloadTemplate::where('workspace_id', $workspace->id)
|
||||
|
|
@ -61,7 +64,7 @@ class WebhookTemplateController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$template = WebhookPayloadTemplate::where('workspace_id', $workspace->id)
|
||||
|
|
@ -69,7 +72,7 @@ class WebhookTemplateController extends Controller
|
|||
->first();
|
||||
|
||||
if (! $template) {
|
||||
return response()->json(['error' => 'Template not found'], 404);
|
||||
return $this->notFoundResponse('Template');
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
|
|
@ -85,7 +88,7 @@ class WebhookTemplateController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
|
|
@ -102,10 +105,9 @@ class WebhookTemplateController extends Controller
|
|||
$validation = $this->templateService->validateTemplate($validated['template'], $format);
|
||||
|
||||
if (! $validation['valid']) {
|
||||
return response()->json([
|
||||
'error' => 'Invalid template',
|
||||
'errors' => $validation['errors'],
|
||||
], 422);
|
||||
return $this->validationErrorResponse([
|
||||
'template' => $validation['errors'],
|
||||
]);
|
||||
}
|
||||
|
||||
$template = WebhookPayloadTemplate::create([
|
||||
|
|
@ -133,7 +135,7 @@ class WebhookTemplateController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$template = WebhookPayloadTemplate::where('workspace_id', $workspace->id)
|
||||
|
|
@ -141,7 +143,7 @@ class WebhookTemplateController extends Controller
|
|||
->first();
|
||||
|
||||
if (! $template) {
|
||||
return response()->json(['error' => 'Template not found'], 404);
|
||||
return $this->notFoundResponse('Template');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
|
|
@ -159,10 +161,9 @@ class WebhookTemplateController extends Controller
|
|||
$validation = $this->templateService->validateTemplate($validated['template'], $format);
|
||||
|
||||
if (! $validation['valid']) {
|
||||
return response()->json([
|
||||
'error' => 'Invalid template',
|
||||
'errors' => $validation['errors'],
|
||||
], 422);
|
||||
return $this->validationErrorResponse([
|
||||
'template' => $validation['errors'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +187,7 @@ class WebhookTemplateController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$template = WebhookPayloadTemplate::where('workspace_id', $workspace->id)
|
||||
|
|
@ -194,12 +195,12 @@ class WebhookTemplateController extends Controller
|
|||
->first();
|
||||
|
||||
if (! $template) {
|
||||
return response()->json(['error' => 'Template not found'], 404);
|
||||
return $this->notFoundResponse('Template');
|
||||
}
|
||||
|
||||
// Don't allow deleting builtin templates
|
||||
if ($template->isBuiltin()) {
|
||||
return response()->json(['error' => 'Built-in templates cannot be deleted'], 403);
|
||||
return $this->forbiddenResponse('Built-in templates cannot be deleted');
|
||||
}
|
||||
|
||||
$template->delete();
|
||||
|
|
@ -255,7 +256,7 @@ class WebhookTemplateController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$template = WebhookPayloadTemplate::where('workspace_id', $workspace->id)
|
||||
|
|
@ -263,7 +264,7 @@ class WebhookTemplateController extends Controller
|
|||
->first();
|
||||
|
||||
if (! $template) {
|
||||
return response()->json(['error' => 'Template not found'], 404);
|
||||
return $this->notFoundResponse('Template');
|
||||
}
|
||||
|
||||
$newName = $request->input('name', $template->name.' (copy)');
|
||||
|
|
@ -282,7 +283,7 @@ class WebhookTemplateController extends Controller
|
|||
$workspace = $request->user()?->defaultHostWorkspace();
|
||||
|
||||
if (! $workspace) {
|
||||
return response()->json(['error' => 'Workspace not found'], 404);
|
||||
return $this->noWorkspaceResponse();
|
||||
}
|
||||
|
||||
$template = WebhookPayloadTemplate::where('workspace_id', $workspace->id)
|
||||
|
|
@ -290,7 +291,7 @@ class WebhookTemplateController extends Controller
|
|||
->first();
|
||||
|
||||
if (! $template) {
|
||||
return response()->json(['error' => 'Template not found'], 404);
|
||||
return $this->notFoundResponse('Template');
|
||||
}
|
||||
|
||||
$template->setAsDefault();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Core\Api\Controllers;
|
||||
|
||||
use Core\Front\Controller;
|
||||
use Core\Api\Concerns\HasApiResponses;
|
||||
use Core\Api\Models\ApiKey;
|
||||
use Core\Mod\Mcp\Models\McpApiRequest;
|
||||
use Core\Mod\Mcp\Models\McpToolCall;
|
||||
|
|
@ -23,6 +24,8 @@ use Symfony\Component\Yaml\Yaml;
|
|||
*/
|
||||
class McpApiController extends Controller
|
||||
{
|
||||
use HasApiResponses;
|
||||
|
||||
/**
|
||||
* List all available MCP servers.
|
||||
*
|
||||
|
|
@ -53,7 +56,7 @@ class McpApiController extends Controller
|
|||
$server = $this->loadServerFull($id);
|
||||
|
||||
if (! $server) {
|
||||
return response()->json(['error' => 'Server not found'], 404);
|
||||
return $this->notFoundResponse('Server');
|
||||
}
|
||||
|
||||
return response()->json($server);
|
||||
|
|
@ -72,7 +75,7 @@ class McpApiController extends Controller
|
|||
$server = $this->loadServerFull($id);
|
||||
|
||||
if (! $server) {
|
||||
return response()->json(['error' => 'Server not found'], 404);
|
||||
return $this->notFoundResponse('Server');
|
||||
}
|
||||
|
||||
$tools = $server['tools'] ?? [];
|
||||
|
|
@ -129,13 +132,13 @@ class McpApiController extends Controller
|
|||
|
||||
$server = $this->loadServerFull($validated['server']);
|
||||
if (! $server) {
|
||||
return response()->json(['error' => 'Server not found'], 404);
|
||||
return $this->notFoundResponse('Server');
|
||||
}
|
||||
|
||||
// Verify tool exists in server definition
|
||||
$toolDef = collect($server['tools'] ?? [])->firstWhere('name', $validated['tool']);
|
||||
if (! $toolDef) {
|
||||
return response()->json(['error' => 'Tool not found'], 404);
|
||||
return $this->notFoundResponse('Tool');
|
||||
}
|
||||
|
||||
// Version resolution
|
||||
|
|
@ -178,15 +181,17 @@ class McpApiController extends Controller
|
|||
);
|
||||
|
||||
if (! empty($validationErrors)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Validation failed',
|
||||
'error_code' => 'VALIDATION_ERROR',
|
||||
'validation_errors' => $validationErrors,
|
||||
'server' => $validated['server'],
|
||||
'tool' => $validated['tool'],
|
||||
'version' => $toolVersion?->version ?? 'unversioned',
|
||||
], 422);
|
||||
return $this->errorResponse(
|
||||
errorCode: 'VALIDATION_ERROR',
|
||||
message: 'Validation failed',
|
||||
meta: [
|
||||
'validation_errors' => $validationErrors,
|
||||
'server' => $validated['server'],
|
||||
'tool' => $validated['tool'],
|
||||
'version' => $toolVersion?->version ?? 'unversioned',
|
||||
],
|
||||
status: 422,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -343,13 +348,13 @@ class McpApiController extends Controller
|
|||
{
|
||||
$serverConfig = $this->loadServerFull($server);
|
||||
if (! $serverConfig) {
|
||||
return response()->json(['error' => 'Server not found'], 404);
|
||||
return $this->notFoundResponse('Server');
|
||||
}
|
||||
|
||||
// Verify tool exists in server definition
|
||||
$toolDef = collect($serverConfig['tools'] ?? [])->firstWhere('name', $tool);
|
||||
if (! $toolDef) {
|
||||
return response()->json(['error' => 'Tool not found'], 404);
|
||||
return $this->notFoundResponse('Tool');
|
||||
}
|
||||
|
||||
$versionService = app(ToolVersionService::class);
|
||||
|
|
@ -374,7 +379,7 @@ class McpApiController extends Controller
|
|||
$toolVersion = $versionService->getToolAtVersion($server, $tool, $version);
|
||||
|
||||
if (! $toolVersion) {
|
||||
return response()->json(['error' => 'Version not found'], 404);
|
||||
return $this->notFoundResponse('Version');
|
||||
}
|
||||
|
||||
$response = response()->json($toolVersion->toApiArray());
|
||||
|
|
@ -399,7 +404,9 @@ class McpApiController extends Controller
|
|||
{
|
||||
// Parse URI format: server://resource/path
|
||||
if (! preg_match('/^([a-z0-9-]+):\/\/(.+)$/', $uri, $matches)) {
|
||||
return response()->json(['error' => 'Invalid resource URI format'], 400);
|
||||
return $this->validationErrorResponse([
|
||||
'uri' => ['Invalid resource URI format. Expected pattern server://resource/path'],
|
||||
], 400);
|
||||
}
|
||||
|
||||
$serverId = $matches[1];
|
||||
|
|
@ -407,7 +414,7 @@ class McpApiController extends Controller
|
|||
|
||||
$server = $this->loadServerFull($serverId);
|
||||
if (! $server) {
|
||||
return response()->json(['error' => 'Server not found'], 404);
|
||||
return $this->notFoundResponse('Server');
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -418,10 +425,14 @@ class McpApiController extends Controller
|
|||
'content' => $result,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'error' => $e->getMessage(),
|
||||
'uri' => $uri,
|
||||
], 500);
|
||||
return $this->errorResponse(
|
||||
errorCode: 'resource_read_error',
|
||||
message: $e->getMessage(),
|
||||
meta: [
|
||||
'uri' => $uri,
|
||||
],
|
||||
status: 500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ class RateLimitExceededException extends HttpException
|
|||
public function render(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'rate_limit_exceeded',
|
||||
'error_code' => 'rate_limit_exceeded',
|
||||
'message' => $this->getMessage(),
|
||||
'retry_after' => $this->rateLimitResult->retryAfter,
|
||||
'limit' => $this->rateLimitResult->limit,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ namespace Core\Api\Middleware;
|
|||
|
||||
use Core\Api\Models\ApiKey;
|
||||
use Core\Api\Services\IpRestrictionService;
|
||||
use Core\Api\Concerns\HasApiResponses;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
|
@ -24,6 +25,8 @@ use Symfony\Component\HttpFoundation\Response;
|
|||
*/
|
||||
class AuthenticateApiKey
|
||||
{
|
||||
use HasApiResponses;
|
||||
|
||||
public function handle(Request $request, Closure $next, ?string $scope = null): Response
|
||||
{
|
||||
$token = $request->bearerToken();
|
||||
|
|
@ -117,10 +120,11 @@ class AuthenticateApiKey
|
|||
*/
|
||||
protected function unauthorized(string $message): Response
|
||||
{
|
||||
return response()->json([
|
||||
'error' => 'unauthorized',
|
||||
'message' => $message,
|
||||
], 401);
|
||||
return $this->errorResponse(
|
||||
errorCode: 'unauthorized',
|
||||
message: $message,
|
||||
status: 401,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -128,9 +132,6 @@ class AuthenticateApiKey
|
|||
*/
|
||||
protected function forbidden(string $message): Response
|
||||
{
|
||||
return response()->json([
|
||||
'error' => 'forbidden',
|
||||
'message' => $message,
|
||||
], 403);
|
||||
return $this->forbiddenResponse($message, status: 403);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Core\Api\Middleware;
|
||||
|
||||
use Core\Api\Models\ApiKey;
|
||||
use Core\Api\Concerns\HasApiResponses;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
|
@ -25,6 +26,8 @@ use Symfony\Component\HttpFoundation\Response;
|
|||
*/
|
||||
class CheckApiScope
|
||||
{
|
||||
use HasApiResponses;
|
||||
|
||||
public function handle(Request $request, Closure $next, string ...$scopes): Response
|
||||
{
|
||||
$apiKey = $request->attributes->get('api_key');
|
||||
|
|
@ -38,12 +41,13 @@ class CheckApiScope
|
|||
// Check all required scopes
|
||||
foreach ($scopes as $scope) {
|
||||
if (! $apiKey->hasScope($scope)) {
|
||||
return response()->json([
|
||||
'error' => 'forbidden',
|
||||
'message' => "API key missing required scope: {$scope}",
|
||||
'required_scopes' => $scopes,
|
||||
'key_scopes' => $apiKey->scopes,
|
||||
], 403);
|
||||
return $this->forbiddenResponse(
|
||||
message: "API key missing required scope: {$scope}",
|
||||
meta: [
|
||||
'required_scopes' => $scopes,
|
||||
'key_scopes' => $apiKey->scopes,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ namespace Core\Api\Middleware;
|
|||
|
||||
use Closure;
|
||||
use Core\Api\Models\ApiKey;
|
||||
use Core\Api\Concerns\HasApiResponses;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
|
|
@ -25,6 +26,8 @@ use Symfony\Component\HttpFoundation\Response;
|
|||
*/
|
||||
class EnforceApiScope
|
||||
{
|
||||
use HasApiResponses;
|
||||
|
||||
/**
|
||||
* HTTP method to required scope mapping.
|
||||
*/
|
||||
|
|
@ -52,12 +55,13 @@ class EnforceApiScope
|
|||
$requiredScope = self::METHOD_SCOPES[$method] ?? ApiKey::SCOPE_READ;
|
||||
|
||||
if (! $apiKey->hasScope($requiredScope)) {
|
||||
return response()->json([
|
||||
'error' => 'forbidden',
|
||||
'message' => "API key missing required scope: {$requiredScope}",
|
||||
'detail' => "{$method} requests require '{$requiredScope}' scope",
|
||||
'key_scopes' => $apiKey->scopes,
|
||||
], 403);
|
||||
return $this->forbiddenResponse(
|
||||
message: "API key missing required scope: {$requiredScope}",
|
||||
meta: [
|
||||
'detail' => "{$method} requests require '{$requiredScope}' scope",
|
||||
'key_scopes' => $apiKey->scopes,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue