Upload Gambar ke Komentar Plane Saat MCP Tidak Bisa Upload
MCP Plane nggak punya aksi upload file dan SSRF guard memang menolak URL privat. Jalan keluarnya turun satu level: FileAsset plus S3Storage dari dalam container.
Ringkasan
Mau nempelin screenshot ke komentar Plane ternyata ribet karena di UI gak ada tombol upload dan MCP server juga gak support upload file. Nyoba akalin pake URL lokal malah ke-blok SSRF guard, pake API key internal juga ditolak. Akhirnya solusinya turun level ke container, copy file ke plane-api, bikin FileAsset dan update komentar pake tag img biar muncul.
Saya lagi ngecek hasil test di server produksi, terus pengen nge-pin screenshot error itu langsung ke komentar Plane di work item yang relevan. Saya buka session, siapin gambar, dan... nggak ada tombol upload. Nggak ada cara bawaan buat naruh gambar dari session lokal ke komentar Plane lewat antarmuka yang saya pakai saat itu. Rasanya kayak mentok di tembok bata.
Saya langsung mikir, "Ah, gampang. Pakai aja MCP server buat automate ini." Saya cek dokumentasi resmi Plane MCP server [9](https://github.com/makeplane/plane-mcp-server), berharap ada action buat upload file. Ternyata nggak ada sama sekali. MCP server memang didesain buat baca dan kelola project, work item, cycle, dan module, tapi fitur upload file sama sekali nggak masuk ke dalam toolset-nya.
Saya coba akal-akalin dengan action workitem_attachment dan parameter upload_from_url. Saya kasih URL lokal atau loopback biar container-nya bisa ambil file dari host saya. Hasilnya? Ditolak mentah-mentah. Ini bukan bug, melainkan SSRF guard yang bekerja sesuai seharusnya [8](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html). Aplikasi memang seharusnya menolak URL internal buat mencegah server-side request forgery, di mana penyerang bisa memanfaatkan aplikasi buat menjangkau jaringan internal atau mesin itu sendiri.
Saya coba lagi lewat endpoint internal /api/ pakai X-Api-Key. Gagal lagi. Ternyata kunci API itu cuma valid di endpoint publik /api/v1, bukan di jalur internal aplikasi [7](https://developers.plane.so/api-reference/introduction). Pemisahan autentikasi ini bikin frustrasi kalo nggak tahu polanya dari awal, karena dokumentasi sering nggak ngejelasin batasan akses antar endpoint internal ini secara eksplisit.
Turun satu level: container adalah API
Kalo abstraksi (MCP) nggak punya tool buat kemampuan tertentu, solusinya ya turun satu level. Karena saya self-hosting Plane di belakang Cloudflare Tunnel, akses eksternal udah di-handle dengan aman. Tapi justru karena itu, akses internal antar service jadi lebih ketat. Untungnya, container-nya sendiri adalah API yang bisa saya akses langsung lewat Docker CLI, tanpa perlu nembus layer network eksternal. Saya nggak perlu melawan SSRF guard; saya cukup masuk ke dalam container.
Saya bikin helper script sederhana: scripts/attach_comment_image.py. Script ini cuma pakai Python stdlib dan Docker CLI, jadi nggak perlu install dependensi berat. Alurnya sangat spesifik dan sengaja dibuat begitu:
1. Saya copy file gambar ke dalam container plane-api.
2. Jalankan Django shell di dalam container itu.
3. Script bikin row FileAsset baru dengan entity_type COMMENT_DESCRIPTION. Ini penting banget: asset harus di-bind ke project_id dan comment_id pas dibuat, bukan ke work item-nya. Kalo salah bind, gambar nggak bakal muncul di tempat yang tepat.
4. Upload byte-nya ke bucket MinIO lewat S3Storage, terus set is_uploaded=True. Proses ini meniru persis apa yang dilakukan Plane saat user upload gambar lewat UI biasa. Bedanya, kita bypass layer HTTP dan langsung eksekusi logic Django-nya, memastikan metadata file tercatat dengan benar di database.
5. Script nge-print satu baris JSON: {"asset_id": "...", "asset_url": "...", "url": "..."} biar bisa diparsing otomatis oleh script lanjutan.
Prosedur tiga langkah, urutannya nggak bisa ditukar
Prosedur akhirnya jadi tiga langkah yang harus berurutan dan nggak bisa ditukar. Pertama, bikin komentarnya dulu lewat MCP pakai action workitem_comment dengan action=create. Asset harus di-bind ke ID komentar yang udah jadi, bukan ke work item.
Kedua, jalanin helper script tadi buat dapetin URL asset dari output JSON-nya.
Ketiga, update komentar tersebut dengan comment_html yang isinya <p><img src="URL" width="640"></p>. Kenapa comment_html? Karena field deskripsi biasa bakal strip tag HTML demi keamanan, sementara comment_html bakal nge-render tag <img> dengan benar di antarmuka Plane.
Cek terakhir yang bikin saya tenang: URL asset itu kalau di-GET anonymous balasnya 401 Unauthorized. Ini ekspektasi yang benar dan bukan error konfigurasi. Plane UI memang didesain buat fetch asset itu pakai session browser yang udah terautentikasi, bukan lewat public link yang terbuka buat siapa aja.
Saya pribadi lebih milih pendekatan "turun satu level" ini daripada memaksakan fitur yang nggak ada di abstraksi MCP. Self-hosting itu privilese sekaligus tanggung jawab. Kita punya akses penuh ke container, jadi kenapa nggak memanfaatkannya dengan cara yang respek terhadap arsitektur keamanan yang udah ada, alih-alih mencoba membobolnya dengan workaround yang rapuh dan susah di-maintain?