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:
Paulo Castellano 2026-07-24 21:29:41 -03:00
parent 564f157e44
commit 3175fbe8c2
6 changed files with 58 additions and 14 deletions

View file

@ -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).",
);
}
});
}
}

View file

@ -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.'),
];
}
}

View file

@ -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();

View file

@ -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),
],
],

View file

@ -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');

View file

@ -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();
});