From dd1056a41314f40c5c4ce73e0129cf08dd51d285 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Fri, 15 May 2026 16:29:05 -0300 Subject: [PATCH] feat(mcp): AttachMediaFromUploadTool attaches uploaded Media by token --- app/Mcp/Servers/TryPostServer.php | 2 + .../Tools/Post/AttachMediaFromUploadTool.php | 74 +++++++++++++++ .../Mcp/AttachMediaFromUploadToolTest.php | 92 +++++++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 app/Mcp/Tools/Post/AttachMediaFromUploadTool.php create mode 100644 tests/Feature/Mcp/AttachMediaFromUploadToolTest.php diff --git a/app/Mcp/Servers/TryPostServer.php b/app/Mcp/Servers/TryPostServer.php index d61aa92c..cd2d1850 100644 --- a/app/Mcp/Servers/TryPostServer.php +++ b/app/Mcp/Servers/TryPostServer.php @@ -12,6 +12,7 @@ use App\Mcp\Tools\Label\ListLabelsTool; use App\Mcp\Tools\Label\UpdateLabelTool; use App\Mcp\Tools\Platform\ListContentTypesTool; +use App\Mcp\Tools\Post\AttachMediaFromUploadTool; use App\Mcp\Tools\Post\AttachMediaFromUrlTool; use App\Mcp\Tools\Post\CreatePostTool; use App\Mcp\Tools\Post\DeletePostTool; @@ -52,6 +53,7 @@ class TryPostServer extends Server DeletePostTool::class, AttachMediaFromUrlTool::class, RequestMediaUploadTool::class, + AttachMediaFromUploadTool::class, GetPostMetricsTool::class, // Platforms (read-only metadata) diff --git a/app/Mcp/Tools/Post/AttachMediaFromUploadTool.php b/app/Mcp/Tools/Post/AttachMediaFromUploadTool.php new file mode 100644 index 00000000..520c0eb7 --- /dev/null +++ b/app/Mcp/Tools/Post/AttachMediaFromUploadTool.php @@ -0,0 +1,74 @@ +validate([ + 'post_id' => ['required', 'uuid'], + 'upload_token' => ['required', 'uuid'], + ]); + + $workspaceId = $request->user()->current_workspace_id; + + $post = Post::where('workspace_id', $workspaceId) + ->find(data_get($validated, 'post_id')); + + if (! $post) { + return Response::error('Post not found.'); + } + + $media = Media::query() + ->where('upload_token', data_get($validated, 'upload_token')) + ->where('mediable_type', Workspace::class) + ->where('mediable_id', $workspaceId) + ->first(); + + if (! $media) { + return Response::error('Upload not found.'); + } + + if (! in_array($media->type, $post->allowedMediaTypes(), true)) { + return Response::error('No enabled platform on this post accepts this media type.'); + } + + $post->appendMedia([[ + 'id' => $media->id, + 'path' => $media->path, + 'url' => $media->url, + 'type' => $media->type, + 'mime_type' => $media->mime_type, + 'original_filename' => $media->original_filename, + ]]); + + $post->refresh()->load(['postPlatforms.socialAccount', 'labels']); + + return Response::structured([ + 'post' => (new PostResource($post))->resolve(), + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'post_id' => $schema->string()->required()->description('UUID of the post to attach the uploaded media to.'), + 'upload_token' => $schema->string()->required()->description('upload_token returned by RequestMediaUploadTool, after the user has POSTed the file to the upload_url.'), + ]; + } +} diff --git a/tests/Feature/Mcp/AttachMediaFromUploadToolTest.php b/tests/Feature/Mcp/AttachMediaFromUploadToolTest.php new file mode 100644 index 00000000..e4481145 --- /dev/null +++ b/tests/Feature/Mcp/AttachMediaFromUploadToolTest.php @@ -0,0 +1,92 @@ +user = User::factory()->create(); + $this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]); + $this->workspace->members()->attach($this->user->id, ['role' => Role::Member->value]); + $this->user->update(['current_workspace_id' => $this->workspace->id]); + + $this->post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + ]); + + $this->token = (string) Str::uuid(); + $this->media = Media::factory()->create([ + 'mediable_type' => Workspace::class, + 'mediable_id' => $this->workspace->id, + 'collection' => 'assets', + 'upload_token' => $this->token, + ]); +}); + +test('attaches the uploaded Media to the post', function () { + $response = TryPostServer::actingAs($this->user) + ->tool(AttachMediaFromUploadTool::class, [ + 'post_id' => $this->post->id, + 'upload_token' => $this->token, + ]); + + $response->assertOk(); + expect($this->post->fresh()->media)->toHaveCount(1); +}); + +test('rejects a token from a different workspace', function () { + $other = User::factory()->create(); + $otherWs = Workspace::factory()->create(['user_id' => $other->id]); + + $foreignToken = (string) Str::uuid(); + Media::factory()->create([ + 'mediable_type' => Workspace::class, + 'mediable_id' => $otherWs->id, + 'collection' => 'assets', + 'upload_token' => $foreignToken, + ]); + + $response = TryPostServer::actingAs($this->user) + ->tool(AttachMediaFromUploadTool::class, [ + 'post_id' => $this->post->id, + 'upload_token' => $foreignToken, + ]); + + $response->assertHasErrors(); + expect($this->post->fresh()->media)->toHaveCount(0); +}); + +test('rejects an unknown upload_token', function () { + $response = TryPostServer::actingAs($this->user) + ->tool(AttachMediaFromUploadTool::class, [ + 'post_id' => $this->post->id, + 'upload_token' => (string) Str::uuid(), + ]); + + $response->assertHasErrors(); +}); + +test('rejects a post from another workspace', function () { + $other = User::factory()->create(); + $otherWs = Workspace::factory()->create(['user_id' => $other->id]); + $otherPost = Post::factory()->create([ + 'workspace_id' => $otherWs->id, + 'user_id' => $other->id, + ]); + + $response = TryPostServer::actingAs($this->user) + ->tool(AttachMediaFromUploadTool::class, [ + 'post_id' => $otherPost->id, + 'upload_token' => $this->token, + ]); + + $response->assertHasErrors(); +});