base-api-scramble-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,968 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MCP Server — Base API Scramble
5
+ * Laravel API Knowledge Base untuk AI (Claude, Cursor, Copilot, dll)
6
+ *
7
+ * Tools:
8
+ * - get_starting_point → Kickoff guide, quick start, stack, konvensi wajib
9
+ * - get_architecture → Arsitektur lengkap, flow request, pattern CRUD
10
+ * - get_structure → Struktur direktori project
11
+ * - search_docs → Full-text search di semua docs
12
+ * - get_doc → Baca 1 doc file lengkap
13
+ * - list_docs → Daftar semua docs yang tersedia
14
+ * - get_conventions → Konvensi & aturan wajib (model, migration, controller, dll)
15
+ * - get_commands → Referensi artisan gc commands
16
+ * - get_api_response_format → Format response API standar
17
+ * - get_query_params → Query parameter yang tersedia
18
+ * - get_file_service → Cara pakai FileService (upload/delete file)
19
+ * - get_export_guide → Excel export system
20
+ */
21
+
22
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
23
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
24
+ import { z } from "zod";
25
+ import { readFileSync, existsSync, readdirSync } from "fs";
26
+ import { join, dirname } from "path";
27
+ import { fileURLToPath } from "url";
28
+
29
+ // ─────────────────────────────────────────────
30
+ // Resolve docs path
31
+ // ─────────────────────────────────────────────
32
+ const __dirname = dirname(fileURLToPath(import.meta.url));
33
+ const DOCS_PATH = join(__dirname, "docs");
34
+
35
+ // ─────────────────────────────────────────────
36
+ // Helper: baca file doc
37
+ // ─────────────────────────────────────────────
38
+ function readDoc(filename) {
39
+ const filePath = join(DOCS_PATH, filename);
40
+ if (!existsSync(filePath)) {
41
+ return `❌ File tidak ditemukan: ${filename}`;
42
+ }
43
+ return readFileSync(filePath, "utf-8");
44
+ }
45
+
46
+ // ─────────────────────────────────────────────
47
+ // Helper: list semua doc files
48
+ // ─────────────────────────────────────────────
49
+ function listDocFiles() {
50
+ if (!existsSync(DOCS_PATH)) return [];
51
+ return readdirSync(DOCS_PATH).filter((f) => f.endsWith(".md"));
52
+ }
53
+
54
+ // ─────────────────────────────────────────────
55
+ // Helper: search di semua docs
56
+ // ─────────────────────────────────────────────
57
+ function searchDocs(query) {
58
+ const files = listDocFiles();
59
+ const queryLower = query.toLowerCase();
60
+ const results = [];
61
+
62
+ for (const file of files) {
63
+ const content = readDoc(file);
64
+ const lines = content.split("\n");
65
+ const matches = [];
66
+
67
+ lines.forEach((line, idx) => {
68
+ if (line.toLowerCase().includes(queryLower)) {
69
+ // ambil context 1 baris sebelum & sesudahnya
70
+ const from = Math.max(0, idx - 1);
71
+ const to = Math.min(lines.length - 1, idx + 1);
72
+ matches.push({
73
+ line: idx + 1,
74
+ context: lines.slice(from, to + 1).join("\n"),
75
+ });
76
+ }
77
+ });
78
+
79
+ if (matches.length > 0) {
80
+ results.push({
81
+ file,
82
+ match_count: matches.length,
83
+ matches: matches.slice(0, 5), // max 5 matches per file
84
+ });
85
+ }
86
+ }
87
+
88
+ return results;
89
+ }
90
+
91
+ // ─────────────────────────────────────────────
92
+ // Buat MCP Server
93
+ // ─────────────────────────────────────────────
94
+ const server = new McpServer({
95
+ name: "base-api-scramble",
96
+ version: "1.0.0",
97
+ });
98
+
99
+ // ══════════════════════════════════════════════
100
+ // TOOL: get_starting_point
101
+ // ══════════════════════════════════════════════
102
+ server.tool(
103
+ "get_starting_point",
104
+ "Dapatkan kickoff guide lengkap: quick start, tech stack, arsitektur, konvensi wajib, dan langkah memulai project baru dari base-api-scramble. GUNAKAN INI PERTAMA KALI saat mulai kerja di project ini.",
105
+ {},
106
+ async () => {
107
+ const content = readDoc("starting_point.md");
108
+ return {
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: content,
113
+ },
114
+ ],
115
+ };
116
+ }
117
+ );
118
+
119
+ // ══════════════════════════════════════════════
120
+ // TOOL: get_architecture
121
+ // ══════════════════════════════════════════════
122
+ server.tool(
123
+ "get_architecture",
124
+ "Dapatkan panduan arsitektur lengkap: flow request, 5 core components, dua workflow (Migration-First & Fields-First), pattern Model/Query/Controller/FormRequest, dan aturan AI. Ideal untuk memahami cara membuat fitur baru.",
125
+ {},
126
+ async () => {
127
+ const content = readDoc("AI-GUIDE-COMPLETE.md");
128
+ return {
129
+ content: [
130
+ {
131
+ type: "text",
132
+ text: content,
133
+ },
134
+ ],
135
+ };
136
+ }
137
+ );
138
+
139
+ // ══════════════════════════════════════════════
140
+ // TOOL: get_structure
141
+ // ══════════════════════════════════════════════
142
+ server.tool(
143
+ "get_structure",
144
+ "Dapatkan struktur direktori lengkap project base-api-scramble: semua folder, file penting, dan penjelasannya.",
145
+ {},
146
+ async () => {
147
+ const content = readDoc("STRUCTURE.md");
148
+ return {
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: content,
153
+ },
154
+ ],
155
+ };
156
+ }
157
+ );
158
+
159
+ // ══════════════════════════════════════════════
160
+ // TOOL: list_docs
161
+ // ══════════════════════════════════════════════
162
+ server.tool(
163
+ "list_docs",
164
+ "Tampilkan daftar semua file dokumentasi yang tersedia di knowledge base ini.",
165
+ {},
166
+ async () => {
167
+ const files = listDocFiles();
168
+
169
+ const descriptions = {
170
+ "starting_point.md":
171
+ "Kickoff guide: quick start, tech stack, konvensi, langkah project baru",
172
+ "AI-GUIDE-COMPLETE.md":
173
+ "Panduan AI lengkap: arsitektur, pattern code, commands, aturan AI",
174
+ "STRUCTURE.md": "Struktur direktori lengkap seluruh project",
175
+ "README.md": "Overview project dengan contoh workflow dan fitur",
176
+ "EXCEL-EXPORT-SERVICE.md": "Dokumentasi Excel export service (PhpSpreadsheet)",
177
+ "EXPORT-DATA-PROVIDERS.md": "Pattern Data Provider untuk export Excel",
178
+ "EXPORT-TEMPLATES.md": "Template Excel: multi-sheet, multi-table, styling",
179
+ "LARAVEL-API-TEST.md": "Panduan testing Laravel API",
180
+ "MAKE-EXPORT-EXCEL-COMMAND.md": "Command generator untuk Excel export",
181
+ };
182
+
183
+ const list = files
184
+ .map((f) => {
185
+ const desc = descriptions[f] || "Dokumentasi tambahan";
186
+ return `• **${f}** — ${desc}`;
187
+ })
188
+ .join("\n");
189
+
190
+ return {
191
+ content: [
192
+ {
193
+ type: "text",
194
+ text: `# Daftar Dokumentasi Base API Scramble\n\nTotal: ${files.length} files\n\n${list}\n\n---\nGunakan tool \`get_doc\` dengan nama file untuk membaca isi lengkapnya.`,
195
+ },
196
+ ],
197
+ };
198
+ }
199
+ );
200
+
201
+ // ══════════════════════════════════════════════
202
+ // TOOL: get_doc
203
+ // ══════════════════════════════════════════════
204
+ server.tool(
205
+ "get_doc",
206
+ "Baca isi lengkap dari satu file dokumentasi. Gunakan list_docs terlebih dahulu untuk melihat file yang tersedia.",
207
+ {
208
+ filename: z
209
+ .string()
210
+ .describe(
211
+ 'Nama file doc yang ingin dibaca. Contoh: "EXCEL-EXPORT-SERVICE.md"'
212
+ ),
213
+ },
214
+ async ({ filename }) => {
215
+ const content = readDoc(filename);
216
+ return {
217
+ content: [
218
+ {
219
+ type: "text",
220
+ text: content,
221
+ },
222
+ ],
223
+ };
224
+ }
225
+ );
226
+
227
+ // ══════════════════════════════════════════════
228
+ // TOOL: search_docs
229
+ // ══════════════════════════════════════════════
230
+ server.tool(
231
+ "search_docs",
232
+ "Cari keyword/topik di semua file dokumentasi. Berguna untuk menemukan informasi spesifik tentang fitur, class, atau konvensi tertentu.",
233
+ {
234
+ query: z
235
+ .string()
236
+ .describe(
237
+ 'Keyword yang dicari. Contoh: "FileService", "baseQuery", "uuid", "gc:from-migration"'
238
+ ),
239
+ },
240
+ async ({ query }) => {
241
+ const results = searchDocs(query);
242
+
243
+ if (results.length === 0) {
244
+ return {
245
+ content: [
246
+ {
247
+ type: "text",
248
+ text: `❌ Tidak ditemukan hasil untuk: "${query}"\n\nCoba keyword lain atau gunakan \`list_docs\` untuk melihat topik yang tersedia.`,
249
+ },
250
+ ],
251
+ };
252
+ }
253
+
254
+ const totalMatches = results.reduce((acc, r) => acc + r.match_count, 0);
255
+ let output = `# Hasil Pencarian: "${query}"\n\nDitemukan **${totalMatches} baris** di **${results.length} file**\n\n---\n\n`;
256
+
257
+ for (const result of results) {
258
+ output += `## 📄 ${result.file} (${result.match_count} match)\n\n`;
259
+ for (const match of result.matches) {
260
+ output += `**Baris ${match.line}:**\n\`\`\`\n${match.context}\n\`\`\`\n\n`;
261
+ }
262
+ output += "---\n\n";
263
+ }
264
+
265
+ return {
266
+ content: [
267
+ {
268
+ type: "text",
269
+ text: output,
270
+ },
271
+ ],
272
+ };
273
+ }
274
+ );
275
+
276
+ // ══════════════════════════════════════════════
277
+ // TOOL: get_conventions
278
+ // ══════════════════════════════════════════════
279
+ server.tool(
280
+ "get_conventions",
281
+ "Dapatkan konvensi & aturan wajib project: cara buat Model (UUID + Queryable), Migration, Query Class, Controller, FormRequest, dan Route. Gunakan ini sebelum membuat kode baru.",
282
+ {},
283
+ async () => {
284
+ const text = `# Konvensi & Pattern Wajib — Base API Scramble
285
+
286
+ ## ⚡ Flow Request
287
+ \`\`\`
288
+ Request → Route → FormRequest (validasi) → Controller → Query Class → Model → ApiResponse
289
+ \`\`\`
290
+
291
+ ---
292
+
293
+ ## 1. Model
294
+ - **WAJIB** pakai \`HasUuids\` (UUID sebagai primary key, BUKAN auto-increment)
295
+ - **WAJIB** pakai \`Queryable\` trait
296
+
297
+ \`\`\`php
298
+ use App\\Traits\\Queryable;
299
+ use Illuminate\\Database\\Eloquent\\Concerns\\HasUuids;
300
+
301
+ class NamaModel extends Model
302
+ {
303
+ use HasFactory, HasUuids, Queryable;
304
+
305
+ protected $fillable = ['field1', 'field2'];
306
+ protected $searchable = ['field1']; // kolom untuk ?search=
307
+ protected $relationIncludes = ['relation1']; // relasi untuk ?include=
308
+ }
309
+ \`\`\`
310
+
311
+ ---
312
+
313
+ ## 2. Migration
314
+ - **WAJIB** pakai UUID primary key
315
+
316
+ \`\`\`php
317
+ Schema::create('table_name', function (Blueprint $table) {
318
+ $table->uuid('id')->primary();
319
+ $table->string('name');
320
+ $table->foreignUuid('parent_id')->constrained('parents')->cascadeOnDelete();
321
+ $table->timestamps();
322
+ });
323
+ \`\`\`
324
+
325
+ ---
326
+
327
+ ## 3. Query Class
328
+ - Lokasi: \`app/Queries/NamaModelQuery.php\`
329
+ - Extends \`QueryBuilder\` dari Spatie
330
+
331
+ \`\`\`php
332
+ class NamaModelQuery extends QueryBuilder
333
+ {
334
+ public function __construct()
335
+ {
336
+ parent::__construct(NamaModel::query());
337
+ $this
338
+ ->allowedIncludes([AllowedInclude::relationship('relation1')])
339
+ ->allowedFilters([
340
+ AllowedFilter::exact('id'),
341
+ AllowedFilter::partial('name'),
342
+ ]);
343
+ }
344
+ }
345
+ \`\`\`
346
+
347
+ ---
348
+
349
+ ## 4. Controller
350
+ - Extends \`Controller\` (sudah include \`ApiResponse\` + \`baseQuery()\`)
351
+ - \`baseQuery()\` menerima **1 argumen** saja: instance Query class
352
+
353
+ \`\`\`php
354
+ class NamaModelController extends Controller
355
+ {
356
+ public function index(IndexRequest $request)
357
+ {
358
+ return $this->responseData(
359
+ $this->baseQuery(new NamaModelQuery()),
360
+ 'NamaModel retrieved successfully'
361
+ );
362
+ }
363
+
364
+ public function show(NamaModel $namaModel)
365
+ {
366
+ return $this->responseSuccess($namaModel, 'NamaModel retrieved successfully');
367
+ }
368
+
369
+ public function store(NamaModelStoreRequest $request)
370
+ {
371
+ $model = NamaModel::create($request->validated());
372
+ return $this->responseCreated($model, 'NamaModel created successfully');
373
+ }
374
+
375
+ public function update(NamaModelUpdateRequest $request, NamaModel $namaModel)
376
+ {
377
+ $namaModel->update($request->validated());
378
+ return $this->responseSuccess($namaModel, 'NamaModel updated successfully');
379
+ }
380
+
381
+ public function destroy(NamaModel $namaModel)
382
+ {
383
+ $namaModel->delete();
384
+ return $this->responseDeleted('NamaModel deleted successfully');
385
+ }
386
+ }
387
+ \`\`\`
388
+
389
+ ---
390
+
391
+ ## 5. FormRequest
392
+ - Gunakan \`@example\` PHPDoc agar Scramble auto-generate contoh di API docs
393
+
394
+ \`\`\`php
395
+ public function rules(): array
396
+ {
397
+ return [
398
+ /** @example John Doe */
399
+ 'name' => 'required|string|max:255',
400
+ /** @example john@example.com */
401
+ 'email' => 'required|email|unique:users,email',
402
+ ];
403
+ }
404
+ \`\`\`
405
+
406
+ ---
407
+
408
+ ## 6. Route
409
+ - Tambahkan di \`routes/api_v1.php\` di dalam group \`auth:sanctum\`
410
+
411
+ \`\`\`php
412
+ Route::apiResource('nama-model', Controllers\\NamaModelController::class);
413
+ \`\`\`
414
+
415
+ ---
416
+
417
+ ## 7. Naming Convention
418
+ | Item | Convention | Contoh |
419
+ |---|---|---|
420
+ | Model | Singular PascalCase | \`ProductCategory\` |
421
+ | Table | Plural snake_case (auto) | \`product_categories\` |
422
+ | Controller | \`{Model}Controller\` | \`ProductCategoryController\` |
423
+ | Query | \`{Model}Query\` | \`ProductCategoryQuery\` |
424
+ | Request | \`{Model}StoreRequest\` | \`ProductCategoryStoreRequest\` |
425
+ | Route | Kebab plural | \`product-categories\` |
426
+
427
+ ---
428
+
429
+ ## ❌ JANGAN PERNAH
430
+ - Generate CRUD manual — **selalu gunakan \`gc\` command**
431
+ - Pakai \`id()->primary()\` — harus \`uuid('id')->primary()\`
432
+ - Pass lebih dari 1 argumen ke \`baseQuery()\`
433
+ - Buat migration tanpa UUID primary key
434
+ `;
435
+
436
+ return {
437
+ content: [{ type: "text", text }],
438
+ };
439
+ }
440
+ );
441
+
442
+ // ══════════════════════════════════════════════
443
+ // TOOL: get_commands
444
+ // ══════════════════════════════════════════════
445
+ server.tool(
446
+ "get_commands",
447
+ "Referensi lengkap artisan GC commands: gc, gc:from-migration, gc:wizard. Beserta contoh penggunaan dan field type mapping.",
448
+ {},
449
+ async () => {
450
+ const text = `# Artisan GC Commands — Referensi Lengkap
451
+
452
+ ## Perintah Utama
453
+
454
+ \`\`\`bash
455
+ # ─── Fields-First ─────────────────────────────────────────────
456
+ php artisan gc Product --fields="name,price:decimal,stock:integer"
457
+ php artisan gc Product --fields="name,price:decimal,image:file" --belongsTo="Category,Brand"
458
+ php artisan gc Product --fields="name,price:decimal" --hasMany="Review"
459
+
460
+ # ─── Migration-First (Recommended untuk schema kompleks) ───────
461
+ php artisan gc:from-migration products # ← nama tabel (plural)
462
+
463
+ # ─── Interactive Wizard ────────────────────────────────────────
464
+ php artisan gc:wizard Product
465
+
466
+ # ─── Extras ───────────────────────────────────────────────────
467
+ php artisan gc Product --fields="name,price" --preview # preview tanpa generate
468
+ php artisan gc Product --fields="name,price" --migration # generate migration only
469
+ php artisan gc:help
470
+
471
+ # ─── Setelah generate ─────────────────────────────────────────
472
+ php artisan migrate
473
+ \`\`\`
474
+
475
+ ---
476
+
477
+ ## File yang di-Generate (6 files)
478
+ \`\`\`
479
+ app/Models/Product.php
480
+ app/Queries/ProductQuery.php
481
+ app/Http/Controllers/Api/V1/ProductController.php
482
+ app/Http/Requests/Api/V1/ProductStoreRequest.php
483
+ app/Http/Requests/Api/V1/ProductUpdateRequest.php
484
+ database/migrations/xxxx_create_products_table.php
485
+ routes/api_v1.php ← route ditambah otomatis
486
+ \`\`\`
487
+
488
+ ---
489
+
490
+ ## Field Type Mapping (auto-detect dari nama field)
491
+
492
+ | Nama Field | Type |
493
+ |---|---|
494
+ | nama, name, title, judul | \`string\` |
495
+ | deskripsi, description, content, konten | \`text\` |
496
+ | harga, price, cost, biaya | \`decimal\` |
497
+ | stok, stock, quantity, jumlah | \`integer\` |
498
+ | gambar, image, photo, foto, file, dokumen | \`file\` |
499
+ | is_*, aktif, active, published | \`boolean\` |
500
+ | *_at, tanggal, date | \`datetime\` |
501
+ | slug | \`string\` |
502
+
503
+ **Tersedia juga:** \`string\`, \`text\`, \`integer\`/\`int\`, \`decimal\`/\`float\`, \`boolean\`/\`bool\`, \`date\`, \`datetime\`, \`file\`/\`image\`
504
+
505
+ ---
506
+
507
+ ## Decision Logic
508
+
509
+ - User sebut "migration" ATAU schema > 5 field → **Workflow A** (\`gc:from-migration\`)
510
+ - Fields < 5 dan jelas → **Workflow B** (\`gc\`)
511
+ - Tidak jelas / butuh guidance → \`gc:wizard\`
512
+
513
+ ---
514
+
515
+ ## Contoh Cepat
516
+
517
+ \`\`\`bash
518
+ # Simple product
519
+ php artisan gc Product --fields="name,price:decimal,stock:integer" && php artisan migrate
520
+
521
+ # Product dengan file upload & relasi
522
+ php artisan gc Product --fields="name,price:decimal,image:file" --belongsTo="Category,Brand" && php artisan migrate
523
+
524
+ # Dari migration yang sudah ada
525
+ php artisan gc:from-migration products && php artisan migrate
526
+
527
+ # Indonesian fields (auto-translate)
528
+ php artisan gc Produk --fields="nama,harga:decimal,stok:integer,gambar:file"
529
+ # → name, price, stock, image
530
+ \`\`\`
531
+ `;
532
+
533
+ return {
534
+ content: [{ type: "text", text }],
535
+ };
536
+ }
537
+ );
538
+
539
+ // ══════════════════════════════════════════════
540
+ // TOOL: get_api_response_format
541
+ // ══════════════════════════════════════════════
542
+ server.tool(
543
+ "get_api_response_format",
544
+ "Dapatkan format response API standar (ApiResponse trait) beserta semua method yang tersedia dan contoh output JSON-nya.",
545
+ {},
546
+ async () => {
547
+ const text = `# API Response Format — Base API Scramble
548
+
549
+ ## Methods Tersedia
550
+
551
+ \`\`\`php
552
+ // Di dalam Controller:
553
+ $this->responseSuccess($data, $message, $statusCode = 200)
554
+ $this->responseData($data, $message) // auto-detect paginator → tambah key pagination
555
+ $this->responseCreated($data, $message) // status 201
556
+ $this->responseDeleted($message) // status 200, data: null
557
+ $this->responseError($message, $code = 400)
558
+ \`\`\`
559
+
560
+ ---
561
+
562
+ ## Format JSON
563
+
564
+ ### ✅ Success (responseSuccess)
565
+ \`\`\`json
566
+ {
567
+ "success": true,
568
+ "message": "Product retrieved successfully",
569
+ "code": 200,
570
+ "data": { "id": "uuid", "name": "iPhone 14" }
571
+ }
572
+ \`\`\`
573
+
574
+ ### ✅ Created (responseCreated)
575
+ \`\`\`json
576
+ {
577
+ "success": true,
578
+ "message": "Product created successfully",
579
+ "code": 201,
580
+ "data": { "id": "uuid", "name": "iPhone 14" }
581
+ }
582
+ \`\`\`
583
+
584
+ ### ✅ Paginated (responseData dengan paginator)
585
+ \`\`\`json
586
+ {
587
+ "success": true,
588
+ "message": "Products retrieved successfully",
589
+ "code": 200,
590
+ "data": [ { "id": "uuid", "name": "iPhone 14" } ],
591
+ "pagination": {
592
+ "current_page": 1,
593
+ "from": 1,
594
+ "to": 10,
595
+ "total": 50,
596
+ "per_page": 10,
597
+ "last_page": 5,
598
+ "next_page": 2,
599
+ "prev_page": null,
600
+ "path": "http://localhost:8000/api/v1/products"
601
+ }
602
+ }
603
+ \`\`\`
604
+
605
+ ### ✅ Deleted (responseDeleted)
606
+ \`\`\`json
607
+ {
608
+ "success": true,
609
+ "message": "Product deleted successfully",
610
+ "code": 200,
611
+ "data": null
612
+ }
613
+ \`\`\`
614
+
615
+ ### ❌ Error (responseError)
616
+ \`\`\`json
617
+ {
618
+ "success": false,
619
+ "message": "Validation failed",
620
+ "code": 422
621
+ }
622
+ \`\`\`
623
+
624
+ ---
625
+
626
+ ## Lokasi File
627
+ \`app/Helpers/ApiResponse.php\`
628
+ `;
629
+
630
+ return {
631
+ content: [{ type: "text", text }],
632
+ };
633
+ }
634
+ );
635
+
636
+ // ══════════════════════════════════════════════
637
+ // TOOL: get_query_params
638
+ // ══════════════════════════════════════════════
639
+ server.tool(
640
+ "get_query_params",
641
+ "Dapatkan daftar lengkap query parameter yang tersedia di semua endpoint index: search, filter, sort, include, paginate, dan cara penggunaannya.",
642
+ {},
643
+ async () => {
644
+ const text = `# Query Parameters — Base API Scramble
645
+
646
+ Semua endpoint \`index\` otomatis support parameter berikut:
647
+
648
+ ## Parameter Lengkap
649
+
650
+ | Parameter | Contoh | Keterangan |
651
+ |---|---|---|
652
+ | \`search\` | \`?search=keyword\` | Full-text search di kolom \`$searchable\` model |
653
+ | \`filter[kolom]\` | \`?filter[name]=john\` | Filter per kolom (exact/partial sesuai Query class) |
654
+ | \`filter[status]\` | \`?filter[status]=active\` | Filter exact |
655
+ | \`sort_by\` | \`?sort_by=name\` | Kolom untuk sorting |
656
+ | \`sort_order\` | \`?sort_order=asc\` | \`asc\` atau \`desc\` (default: \`desc\`) |
657
+ | \`include\` | \`?include=category,brand\` | Eager load relasi (dari \`$relationIncludes\`) |
658
+ | \`paginate\` | \`?paginate=20\` | Items per page (aktifkan pagination) |
659
+ | \`paginate=false\` | \`?paginate=false\` | Ambil semua data tanpa pagination |
660
+ | \`page\` | \`?page=2\` | Nomor halaman |
661
+ | \`first\` | \`?first=true\` | Ambil hanya 1 data pertama |
662
+ | \`fields[model]\` | \`?fields[products]=id,name\` | Pilih kolom yang dikembalikan |
663
+
664
+ ---
665
+
666
+ ## Contoh Kombinasi
667
+
668
+ \`\`\`
669
+ GET /api/v1/products?search=iphone&filter[category_id]=uuid&sort_by=price&sort_order=asc&paginate=15&include=category
670
+
671
+ GET /api/v1/users?search=john&filter[status]=active&include=roles,permissions&paginate=25
672
+
673
+ GET /api/v1/products?paginate=false # semua data, tanpa pagination
674
+
675
+ GET /api/v1/products?first=true # hanya 1 data pertama
676
+ \`\`\`
677
+
678
+ ---
679
+
680
+ ## Sumber Parameter
681
+
682
+ | Parameter | Dihandle oleh |
683
+ |---|---|
684
+ | \`search\`, \`sort_by\`, \`sort_order\`, \`paginate\`, \`first\` | \`baseQuery()\` di Controller (via \`Queryable\` trait) |
685
+ | \`filter[]\`, \`include\`, \`sort\`, \`fields[]\` | Spatie QueryBuilder di Query class |
686
+
687
+ ---
688
+
689
+ ## Default Behaviour
690
+ - Pagination default: **10 per page**
691
+ - Sort default: **created_at DESC**
692
+ - Tanpa \`?paginate\`: data dikembalikan tanpa pagination (array biasa)
693
+ `;
694
+
695
+ return {
696
+ content: [{ type: "text", text }],
697
+ };
698
+ }
699
+ );
700
+
701
+ // ══════════════════════════════════════════════
702
+ // TOOL: get_file_service
703
+ // ══════════════════════════════════════════════
704
+ server.tool(
705
+ "get_file_service",
706
+ "Panduan cara menggunakan FileService untuk upload file dengan kompresi otomatis, delete file, dan integrasi ke Controller.",
707
+ {},
708
+ async () => {
709
+ const text = `# FileService — Panduan Upload & Delete File
710
+
711
+ ## Lokasi
712
+ \`app/Services/FileService.php\`
713
+
714
+ ---
715
+
716
+ ## Upload File (saveFileComplete)
717
+
718
+ \`\`\`php
719
+ use App\\Services\\FileService;
720
+
721
+ class ProductController extends Controller
722
+ {
723
+ private FileService $fileService;
724
+
725
+ public function __construct()
726
+ {
727
+ $this->fileService = new FileService();
728
+ }
729
+
730
+ public function store(ProductStoreRequest $request)
731
+ {
732
+ $data = $request->validated();
733
+
734
+ if ($request->hasFile('image')) {
735
+ $file = $this->fileService->saveFileComplete(
736
+ file: $request->file('image'),
737
+ folder: 'products', // disimpan di storage/app/products/
738
+ fileName: 'product_' . time(), // nama file (tanpa ekstensi)
739
+ is_compressed: true, // kompresi otomatis (jpg/jpeg/png/webp)
740
+ );
741
+ $data['image'] = $file['name']; // simpan nama file ke DB
742
+ }
743
+
744
+ return $this->responseCreated(Product::create($data), 'Product created');
745
+ }
746
+ }
747
+ \`\`\`
748
+
749
+ **Return value \`saveFileComplete()\`:**
750
+ \`\`\`php
751
+ [
752
+ 'name' => 'product_1234567890.jpg', // nama file yang disimpan
753
+ 'path' => '/storage/products/...', // path lengkap
754
+ 'size' => 204800, // ukuran dalam bytes
755
+ ]
756
+ \`\`\`
757
+
758
+ ---
759
+
760
+ ## Delete File
761
+
762
+ \`\`\`php
763
+ // Hapus 1 file
764
+ $this->fileService->deleteFile(
765
+ folder: 'products',
766
+ fileName: $product->image // nama file yang tersimpan di DB
767
+ );
768
+ \`\`\`
769
+
770
+ ---
771
+
772
+ ## Update File (pattern lengkap)
773
+
774
+ \`\`\`php
775
+ public function update(ProductUpdateRequest $request, Product $product)
776
+ {
777
+ $data = $request->validated();
778
+
779
+ // Case 1: Field image di-set null → hapus file lama
780
+ if ($request->input('image') == null && $product->image) {
781
+ $this->fileService->deleteFile(folder: 'products', fileName: $product->image);
782
+ $data['image'] = '';
783
+ }
784
+
785
+ // Case 2: Ada file baru → hapus lama, simpan baru
786
+ if ($request->hasFile('image')) {
787
+ if ($product->image) {
788
+ $this->fileService->deleteFile(folder: 'products', fileName: $product->image);
789
+ }
790
+ $file = $this->fileService->saveFileComplete(
791
+ file: $request->file('image'),
792
+ folder: 'products',
793
+ fileName: 'product_' . time(),
794
+ is_compressed: true,
795
+ );
796
+ $data['image'] = $file['name'];
797
+ }
798
+
799
+ $product->update($data);
800
+ return $this->responseSuccess($product, 'Product updated');
801
+ }
802
+ \`\`\`
803
+
804
+ ---
805
+
806
+ ## Accessor untuk URL (di Model)
807
+
808
+ \`\`\`php
809
+ protected $appends = ['image_url'];
810
+
811
+ public function getImageUrlAttribute(): ?string
812
+ {
813
+ return $this->image
814
+ ? asset('uploads/products/' . $this->image)
815
+ : null;
816
+ }
817
+ \`\`\`
818
+
819
+ ---
820
+
821
+ ## Storage Support
822
+ - **Local:** \`storage/app/{folder}/\`
823
+ - **S3:** \`filemanager/{folder}/\` (konfigurasi di \`.env\`)
824
+
825
+ ---
826
+
827
+ ## Kompresi Otomatis
828
+ - Menggunakan **Imagick**
829
+ - Support: \`jpg\`, \`jpeg\`, \`png\`, \`webp\`
830
+ - Aktifkan: \`is_compressed: true\`
831
+ `;
832
+
833
+ return {
834
+ content: [{ type: "text", text }],
835
+ };
836
+ }
837
+ );
838
+
839
+ // ══════════════════════════════════════════════
840
+ // TOOL: get_export_guide
841
+ // ══════════════════════════════════════════════
842
+ server.tool(
843
+ "get_export_guide",
844
+ "Panduan sistem Excel export: BaseExport, Data Provider pattern, template multi-sheet/multi-table, dan konfigurasi di config/export_info.php.",
845
+ {},
846
+ async () => {
847
+ // Coba baca dari docs dulu, fallback ke summary
848
+ const files = ["EXCEL-EXPORT-SERVICE.md", "EXPORT-DATA-PROVIDERS.md"];
849
+ let content = "";
850
+
851
+ for (const f of files) {
852
+ const doc = readDoc(f);
853
+ if (!doc.startsWith("❌")) {
854
+ content += `\n\n---\n# ${f}\n\n${doc}`;
855
+ }
856
+ }
857
+
858
+ if (!content) {
859
+ content = `# Excel Export System — Base API Scramble
860
+
861
+ ## Overview
862
+ System export Excel menggunakan **PhpSpreadsheet** dengan pattern **Data Provider**.
863
+
864
+ ## Komponen Utama
865
+ - \`app/Exports/BaseExport.php\` — Abstract base class
866
+ - \`app/Services/ExcelExportService.php\` — Service layer
867
+ - \`config/export_info.php\` — Konfigurasi styling & metadata
868
+ - Data Provider pattern (interface-based)
869
+
870
+ ## Generate Export Module
871
+ \`\`\`bash
872
+ php artisan make:export-excel NamaModule
873
+ \`\`\`
874
+
875
+ Gunakan \`get_doc\` dengan \`EXCEL-EXPORT-SERVICE.md\` untuk dokumentasi lengkap.
876
+ `;
877
+ }
878
+
879
+ return {
880
+ content: [{ type: "text", text: content }],
881
+ };
882
+ }
883
+ );
884
+
885
+ // ══════════════════════════════════════════════
886
+ // TOOL: get_auth_endpoints
887
+ // ══════════════════════════════════════════════
888
+ server.tool(
889
+ "get_auth_endpoints",
890
+ "Daftar endpoint autentikasi dan RBAC yang sudah tersedia: login, logout, profile, role, permission management.",
891
+ {},
892
+ async () => {
893
+ const text = `# Endpoint Bawaan — Base API Scramble
894
+
895
+ ## Authentication (Sanctum)
896
+ | Method | Endpoint | Keterangan |
897
+ |---|---|---|
898
+ | POST | \`/api/v1/login\` | Login, dapat Bearer token |
899
+ | POST | \`/api/v1/logout\` | Logout (auth required) |
900
+ | GET | \`/api/v1/me\` | Profile user yang sedang login |
901
+ | PUT | \`/api/v1/me\` | Update profile |
902
+ | PUT | \`/api/v1/change-password\` | Ganti password |
903
+
904
+ **Default credentials:**
905
+ | Email | Password |
906
+ |---|---|
907
+ | \`admin@gmail.com\` | \`password\` |
908
+ | \`dev@gotrasoft.com\` | \`gotradev\` |
909
+
910
+ ---
911
+
912
+ ## Role & Permission (Spatie RBAC)
913
+ | Method | Endpoint | Keterangan |
914
+ |---|---|---|
915
+ | GET/POST/PUT/DELETE | \`/api/v1/role\` | CRUD Role |
916
+ | GET/POST/PUT/DELETE | \`/api/v1/permission\` | CRUD Permission |
917
+ | POST | \`/api/v1/user-role/attach\` | Assign role ke user |
918
+ | POST | \`/api/v1/user-role/detach\` | Lepas role dari user |
919
+ | POST | \`/api/v1/user-role/sync-roles\` | Sync roles user |
920
+ | POST | \`/api/v1/user-role/sync-users\` | Sync users role |
921
+ | POST | \`/api/v1/role-permission/attach\` | Assign permission ke role |
922
+ | POST | \`/api/v1/role-permission/detach\` | Lepas permission dari role |
923
+ | POST | \`/api/v1/role-permission/sync-permissions\` | Sync permissions role |
924
+ | POST | \`/api/v1/role-permission/sync-roles\` | Sync roles permission |
925
+
926
+ ---
927
+
928
+ ## File Management
929
+ | Method | Endpoint | Keterangan |
930
+ |---|---|---|
931
+ | GET/POST/PUT/DELETE | \`/api/v1/file\` | CRUD File (soft-delete) |
932
+ | GET/POST/PUT/DELETE | \`/api/v1/folder\` | CRUD Folder |
933
+ | POST | \`/api/v1/file/restore/{id}\` | Restore file yang di-soft-delete |
934
+
935
+ ---
936
+
937
+ ## Activity Log
938
+ | Method | Endpoint | Keterangan |
939
+ |---|---|---|
940
+ | GET | \`/api/v1/log-activity\` | Lihat semua audit log |
941
+
942
+ ---
943
+
944
+ ## API Docs
945
+ - **Scramble:** \`http://localhost:8000/docs/api\`
946
+ - **Base URL:** \`http://localhost:8000/api/v1\`
947
+
948
+ ---
949
+
950
+ ## Headers Wajib
951
+ \`\`\`
952
+ Authorization: Bearer {token}
953
+ Content-Type: application/json
954
+ Accept: application/json
955
+ \`\`\`
956
+ `;
957
+
958
+ return {
959
+ content: [{ type: "text", text }],
960
+ };
961
+ }
962
+ );
963
+
964
+ // ─────────────────────────────────────────────
965
+ // Start server
966
+ // ─────────────────────────────────────────────
967
+ const transport = new StdioServerTransport();
968
+ await server.connect(transport);