Centralize MCP upload size caps on trypost.media.
Drop the separate ai.mcp.upload.max_size_mb default and reuse Media\Type limits so MCP matches web/API (1GB video ceiling with per-type enforcement). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
564f157e44
commit
3175fbe8c2
6 changed files with 58 additions and 14 deletions
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use App\Enums\Media\Type as MediaType;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class StoreUploadRequest extends FormRequest
|
||||
{
|
||||
|
|
@ -25,15 +26,42 @@ public function rules(): array
|
|||
MediaType::Document->allowedMimeTypes(),
|
||||
));
|
||||
|
||||
$maxKb = (int) config('ai.mcp.upload.max_size_mb') * 1024;
|
||||
|
||||
return [
|
||||
// Largest per-type cap as the FormRequest upper bound; per-type
|
||||
// enforcement runs in withValidator() below — same pattern as
|
||||
// StoreMediaRequest + PostController::storeMedia.
|
||||
'media' => [
|
||||
'required',
|
||||
'file',
|
||||
"max:{$maxKb}",
|
||||
'max:'.MediaType::Video->maxSizeInKb(),
|
||||
"mimetypes:{$allowed}",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
$validator->after(function (Validator $validator): void {
|
||||
$file = $this->file('media');
|
||||
|
||||
if ($file === null || $validator->errors()->isNotEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = MediaType::fromMime((string) $file->getMimeType());
|
||||
|
||||
if ($type === null) {
|
||||
$validator->errors()->add('media', 'This file type is not supported.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($file->getSize() > $type->maxSizeInBytes()) {
|
||||
$validator->errors()->add(
|
||||
'media',
|
||||
"File size exceeds the maximum allowed for {$type->value} ({$type->maxSizeInMb()} MB).",
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ public function schema(JsonSchema $schema): array
|
|||
'alt' => $u->string()->description('Optional accessibility alt text for the image (ignored for video/PDF, which have no alt text).'),
|
||||
]))
|
||||
->required()
|
||||
->description('Media to attach. Max 10 per call, 50MB per file. Allowed types: image/jpeg, image/png, image/gif, image/webp, video/mp4, video/quicktime, application/pdf.'),
|
||||
->description('Media to attach. Max 10 per call. Per-type size caps match config trypost.media (image/video/document). Allowed types: image/jpeg, image/png, image/gif, image/webp, video/mp4, video/quicktime, application/pdf.'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Mcp\Tools\Post;
|
||||
|
||||
use App\Enums\Media\Type as MediaType;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
|
@ -14,7 +15,7 @@
|
|||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Description('Issue a one-shot signed POST URL that lets the user upload a local file (image, video, or PDF document, up to the workspace upload cap — 300 MB by default) directly to this workspace. Returns an upload_token and upload_url. Hand the URL to the user (e.g. as a curl command with `-F media=@path/to/file`) or to the MCP client. After upload, call AttachMediaFromUploadTool(post_id, upload_token) to attach the result to a post.')]
|
||||
#[Description('Issue a one-shot signed POST URL that lets the user upload a local file (image, video, or PDF document) directly to this workspace. Size caps match web/API media limits (video up to the configured video max; images and PDFs use their own smaller caps). Returns an upload_token, upload_url, and max_bytes. Hand the URL to the user (e.g. as a curl command with `-F media=@path/to/file`) or to the MCP client. After upload, call AttachMediaFromUploadTool(post_id, upload_token) to attach the result to a post.')]
|
||||
class RequestMediaUploadTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): Response|ResponseFactory
|
||||
|
|
@ -22,7 +23,8 @@ public function handle(Request $request): Response|ResponseFactory
|
|||
$user = $request->user();
|
||||
$workspaceId = $user->current_workspace_id;
|
||||
|
||||
$maxBytes = (int) config('ai.mcp.upload.max_size_mb') * 1024 * 1024;
|
||||
// Same ceiling the web/API FormRequests use (largest per-type cap).
|
||||
$maxBytes = MediaType::Video->maxSizeInBytes();
|
||||
$ttlMinutes = (int) config('ai.mcp.upload.url_ttl_minutes');
|
||||
|
||||
$token = (string) Str::uuid();
|
||||
|
|
|
|||
|
|
@ -44,16 +44,14 @@
|
|||
| MCP Media Upload
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Settings for the MCP signed-URL upload flow. The hard cap is enforced
|
||||
| per upload regardless of media type and is intentionally smaller than
|
||||
| the per-type caps in config('trypost.media') because uploads flow
|
||||
| through the app server.
|
||||
| Settings for the MCP signed-URL upload flow. Size caps come from
|
||||
| config('trypost.media.max_size_mb') via Media\Type — same source as
|
||||
| web and REST API uploads.
|
||||
|
|
||||
*/
|
||||
|
||||
'mcp' => [
|
||||
'upload' => [
|
||||
'max_size_mb' => (int) env('MCP_UPLOAD_MAX_SIZE_MB', 300),
|
||||
'url_ttl_minutes' => (int) env('MCP_UPLOAD_URL_TTL_MINUTES', 15),
|
||||
],
|
||||
],
|
||||
|
|
|
|||
|
|
@ -102,8 +102,8 @@ function signedUploadUrl(Workspace $ws, string $token, ?int $expiresInMinutes =
|
|||
expect(Media::where('upload_token', $token)->count())->toBe(1);
|
||||
});
|
||||
|
||||
test('rejects file larger than the MCP upload cap', function () {
|
||||
config(['ai.mcp.upload.max_size_mb' => 1]);
|
||||
test('rejects file larger than the per-type media cap', function () {
|
||||
config(['trypost.media.max_size_mb.video' => 1]);
|
||||
|
||||
$token = (string) Str::uuid();
|
||||
$file = UploadedFile::fake()->create('huge.mp4', 1024 + 1, 'video/mp4');
|
||||
|
|
@ -114,6 +114,21 @@ function signedUploadUrl(Workspace $ws, string $token, ?int $expiresInMinutes =
|
|||
expect(Media::where('upload_token', $token)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('rejects an image larger than the image media cap even when under the video ceiling', function () {
|
||||
config([
|
||||
'trypost.media.max_size_mb.image' => 1,
|
||||
'trypost.media.max_size_mb.video' => 1024,
|
||||
]);
|
||||
|
||||
$token = (string) Str::uuid();
|
||||
$file = UploadedFile::fake()->create('big.jpg', 1024 + 1, 'image/jpeg');
|
||||
|
||||
$response = $this->postJson(signedUploadUrl($this->workspace, $token), ['media' => $file]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
expect(Media::where('upload_token', $token)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('rejects disallowed mime type', function () {
|
||||
$token = (string) Str::uuid();
|
||||
$file = UploadedFile::fake()->create('evil.exe', 10, 'application/octet-stream');
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\Media\Type as MediaType;
|
||||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Mcp\Servers\TryPostServer;
|
||||
use App\Mcp\Tools\Post\RequestMediaUploadTool;
|
||||
|
|
@ -26,7 +27,7 @@
|
|||
$json->has('upload_token')
|
||||
->has('upload_url')
|
||||
->has('expires_at')
|
||||
->where('max_bytes', 300 * 1024 * 1024)
|
||||
->where('max_bytes', MediaType::Video->maxSizeInBytes())
|
||||
->where('field_name', 'media')
|
||||
->etc();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue