@donkeylabs/server 0.1.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.
@@ -0,0 +1,3238 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * @donkeylabs/server MCP Server
4
+ *
5
+ * Provides tools for AI assistants to create and manage plugins,
6
+ * routes, and server code following project conventions.
7
+ */
8
+
9
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
+ import {
12
+ CallToolRequestSchema,
13
+ ListToolsRequestSchema,
14
+ ListResourcesRequestSchema,
15
+ ReadResourceRequestSchema,
16
+ type Tool,
17
+ } from "@modelcontextprotocol/sdk/types.js";
18
+ import { mkdir, writeFile, readFile, readdir } from "node:fs/promises";
19
+ import { join, relative } from "node:path";
20
+ import { existsSync } from "node:fs";
21
+
22
+ // ============================================
23
+ // Tool Definitions
24
+ // ============================================
25
+
26
+ const tools: Tool[] = [
27
+ {
28
+ name: "init_project",
29
+ description: `Initialize a new @donkeylabs/server project. Creates the basic structure with:
30
+ - src/index.ts (server entry point)
31
+ - src/db.ts (SQLite database setup)
32
+ - donkeylabs.config.ts (project config)
33
+ - package.json with dependencies
34
+ - tsconfig.json
35
+
36
+ Run this first before creating plugins or routes.`,
37
+ inputSchema: {
38
+ type: "object",
39
+ properties: {
40
+ projectName: {
41
+ type: "string",
42
+ description: "Name for the project (used in package.json)",
43
+ },
44
+ useDatabase: {
45
+ type: "boolean",
46
+ description: "Set up SQLite database (default: true)",
47
+ default: true,
48
+ },
49
+ },
50
+ },
51
+ },
52
+ {
53
+ name: "create_plugin",
54
+ description: `Create a new plugin with correct directory structure.
55
+
56
+ AVAILABLE IN PLUGIN SERVICE CONTEXT (ctx):
57
+ - ctx.db: Kysely database with your schema types
58
+ - ctx.deps: Access to dependency plugin services (e.g., ctx.deps.auth)
59
+ - ctx.core.logger: Structured logging (info, warn, error, debug)
60
+ - ctx.core.cache: In-memory caching with TTL
61
+ - ctx.core.events: Pub/sub event system
62
+ - ctx.core.jobs: Background job processing
63
+ - ctx.core.cron: Scheduled tasks (cron expressions)
64
+ - ctx.core.sse: Server-sent events for real-time updates
65
+ - ctx.core.rateLimiter: Rate limiting per IP/key
66
+ - ctx.errors: Error factories (BadRequest, Unauthorized, NotFound, etc.)
67
+
68
+ Use these services to build robust, production-ready plugins.`,
69
+ inputSchema: {
70
+ type: "object",
71
+ properties: {
72
+ name: {
73
+ type: "string",
74
+ description: "Plugin name (lowercase, alphanumeric with dashes). Example: 'notifications' or 'user-preferences'",
75
+ },
76
+ hasSchema: {
77
+ type: "boolean",
78
+ description: "Whether this plugin needs a database schema and migrations",
79
+ default: false,
80
+ },
81
+ dependencies: {
82
+ type: "array",
83
+ items: { type: "string" },
84
+ description: "Names of other plugins this depends on. Example: ['auth', 'notifications']",
85
+ default: [],
86
+ },
87
+ serviceMethods: {
88
+ type: "array",
89
+ items: { type: "string" },
90
+ description: "Names of service methods to scaffold. Example: ['sendEmail', 'getNotifications']",
91
+ default: [],
92
+ },
93
+ },
94
+ required: ["name"],
95
+ },
96
+ },
97
+ {
98
+ name: "add_route",
99
+ description:
100
+ "Add a new route to an existing router file. Generates properly typed route with input/output schemas.",
101
+ inputSchema: {
102
+ type: "object",
103
+ properties: {
104
+ routerFile: {
105
+ type: "string",
106
+ description: "Path to the router file (relative to project root). Example: 'src/index.ts'",
107
+ },
108
+ routeName: {
109
+ type: "string",
110
+ description: "Name of the route (will be prefixed with router namespace). Example: 'getUser'",
111
+ },
112
+ handlerType: {
113
+ type: "string",
114
+ enum: ["typed", "raw"],
115
+ description: "Handler type: 'typed' for JSON-RPC style, 'raw' for direct Request/Response",
116
+ default: "typed",
117
+ },
118
+ inputFields: {
119
+ type: "array",
120
+ items: {
121
+ type: "object",
122
+ properties: {
123
+ name: { type: "string" },
124
+ type: { type: "string", enum: ["string", "number", "boolean", "object", "array"] },
125
+ optional: { type: "boolean", default: false },
126
+ },
127
+ },
128
+ description: "Input schema fields for typed handlers",
129
+ },
130
+ outputFields: {
131
+ type: "array",
132
+ items: {
133
+ type: "object",
134
+ properties: {
135
+ name: { type: "string" },
136
+ type: { type: "string", enum: ["string", "number", "boolean", "object", "array"] },
137
+ },
138
+ },
139
+ description: "Output schema fields for typed handlers",
140
+ },
141
+ description: {
142
+ type: "string",
143
+ description: "Description of what this route does",
144
+ },
145
+ },
146
+ required: ["routerFile", "routeName"],
147
+ },
148
+ },
149
+ {
150
+ name: "add_migration",
151
+ description:
152
+ "Create a new migration file for a plugin with proper sequential numbering.",
153
+ inputSchema: {
154
+ type: "object",
155
+ properties: {
156
+ pluginName: {
157
+ type: "string",
158
+ description: "Name of the plugin to add migration to",
159
+ },
160
+ migrationName: {
161
+ type: "string",
162
+ description: "Name for the migration (snake_case). Example: 'add_email_column'",
163
+ },
164
+ upSql: {
165
+ type: "string",
166
+ description: "SQL or Kysely code for the 'up' migration",
167
+ },
168
+ downSql: {
169
+ type: "string",
170
+ description: "SQL or Kysely code for the 'down' migration",
171
+ },
172
+ },
173
+ required: ["pluginName", "migrationName"],
174
+ },
175
+ },
176
+ {
177
+ name: "generate_types",
178
+ description:
179
+ "Run type generation to update registry.d.ts and context.d.ts after making changes to plugins or handlers.",
180
+ inputSchema: {
181
+ type: "object",
182
+ properties: {},
183
+ },
184
+ },
185
+ {
186
+ name: "list_plugins",
187
+ description:
188
+ "List all plugins in the project with their dependencies and service methods.",
189
+ inputSchema: {
190
+ type: "object",
191
+ properties: {},
192
+ },
193
+ },
194
+ {
195
+ name: "get_project_info",
196
+ description:
197
+ "Get information about the project structure, available handlers, middleware, and configuration.",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {},
201
+ },
202
+ },
203
+ {
204
+ name: "add_service_method",
205
+ description:
206
+ "Add a new method to an existing plugin's service interface and implementation.",
207
+ inputSchema: {
208
+ type: "object",
209
+ properties: {
210
+ pluginName: {
211
+ type: "string",
212
+ description: "Name of the plugin to add the method to",
213
+ },
214
+ methodName: {
215
+ type: "string",
216
+ description: "Name of the method (camelCase). Example: 'sendNotification'",
217
+ },
218
+ params: {
219
+ type: "array",
220
+ items: {
221
+ type: "object",
222
+ properties: {
223
+ name: { type: "string" },
224
+ type: { type: "string" },
225
+ },
226
+ },
227
+ description: "Method parameters",
228
+ },
229
+ returnType: {
230
+ type: "string",
231
+ description: "Return type. Example: 'Promise<boolean>'",
232
+ },
233
+ implementation: {
234
+ type: "string",
235
+ description: "Method implementation code (optional, will scaffold if not provided)",
236
+ },
237
+ },
238
+ required: ["pluginName", "methodName"],
239
+ },
240
+ },
241
+ {
242
+ name: "create_router",
243
+ description:
244
+ "Create a new router file with a namespace. Use this when you need to organize routes into separate files.",
245
+ inputSchema: {
246
+ type: "object",
247
+ properties: {
248
+ fileName: {
249
+ type: "string",
250
+ description: "Name for the router file (without extension). Example: 'users' creates 'src/routes/users.ts'",
251
+ },
252
+ namespace: {
253
+ type: "string",
254
+ description: "Router namespace (prefix for all routes). Example: 'users' means routes are 'users.list', 'users.get', etc.",
255
+ },
256
+ initialRoutes: {
257
+ type: "array",
258
+ items: { type: "string" },
259
+ description: "Names of initial routes to create. Example: ['list', 'get', 'create']",
260
+ default: [],
261
+ },
262
+ },
263
+ required: ["fileName", "namespace"],
264
+ },
265
+ },
266
+ {
267
+ name: "validate_project",
268
+ description:
269
+ "Check the project for common issues: missing dependencies, invalid plugin structure, import errors, etc.",
270
+ inputSchema: {
271
+ type: "object",
272
+ properties: {},
273
+ },
274
+ },
275
+ {
276
+ name: "register_plugin",
277
+ description:
278
+ "Register a plugin in the server's index.ts file. Adds the import and registerPlugin call.",
279
+ inputSchema: {
280
+ type: "object",
281
+ properties: {
282
+ pluginName: {
283
+ type: "string",
284
+ description: "Name of the plugin to register (must exist in src/plugins/)",
285
+ },
286
+ configOptions: {
287
+ type: "object",
288
+ description: "Optional configuration object to pass to the plugin",
289
+ },
290
+ },
291
+ required: ["pluginName"],
292
+ },
293
+ },
294
+ {
295
+ name: "run_migrations",
296
+ description:
297
+ "Run database migrations for one or all plugins. Creates tables and applies schema changes.",
298
+ inputSchema: {
299
+ type: "object",
300
+ properties: {
301
+ pluginName: {
302
+ type: "string",
303
+ description: "Plugin name to run migrations for (optional - runs all if not specified)",
304
+ },
305
+ direction: {
306
+ type: "string",
307
+ enum: ["up", "down"],
308
+ description: "Migration direction: 'up' to apply, 'down' to rollback (default: up)",
309
+ default: "up",
310
+ },
311
+ },
312
+ },
313
+ },
314
+ {
315
+ name: "setup_database",
316
+ description:
317
+ "Initialize or check database setup. Creates the database file and verifies the connection.",
318
+ inputSchema: {
319
+ type: "object",
320
+ properties: {
321
+ dbPath: {
322
+ type: "string",
323
+ description: "Path to SQLite database file (default: app.db)",
324
+ default: "app.db",
325
+ },
326
+ },
327
+ },
328
+ },
329
+ {
330
+ name: "list_routes",
331
+ description:
332
+ "List all routes in the project. Shows namespace, route name, handler type, and file path. Use to avoid duplicates.",
333
+ inputSchema: {
334
+ type: "object",
335
+ properties: {
336
+ namespace: {
337
+ type: "string",
338
+ description: "Filter by namespace (optional)",
339
+ },
340
+ },
341
+ },
342
+ },
343
+ {
344
+ name: "add_route_file",
345
+ description: `Create a new route with proper directory structure.
346
+
347
+ ROUTE HANDLER CONTEXT (ctx) - Available in all handlers:
348
+ - ctx.db: Kysely database with type-safe queries
349
+ - ctx.plugins: All registered plugin services (e.g., ctx.plugins.auth, ctx.plugins.users)
350
+ - ctx.core.logger: Structured logging
351
+ - ctx.core.cache: Caching with TTL
352
+ - ctx.core.events: Pub/sub events
353
+ - ctx.core.jobs: Background job processing
354
+ - ctx.core.cron: Scheduled tasks
355
+ - ctx.core.sse: Server-sent events
356
+ - ctx.core.rateLimiter: Rate limiting
357
+ - ctx.errors: Error factories (BadRequest, NotFound, etc.)
358
+ - ctx.ip: Client IP address
359
+ - ctx.requestId: Request tracking ID
360
+ - ctx.user: Authenticated user (if set by auth middleware)
361
+
362
+ Creates /src/routes/<namespace>/<route>/handler.ts and optional test.ts.`,
363
+ inputSchema: {
364
+ type: "object",
365
+ properties: {
366
+ namespace: {
367
+ type: "string",
368
+ description: "Route namespace (e.g., 'users', 'posts'). Creates /src/routes/<namespace>/",
369
+ },
370
+ routeName: {
371
+ type: "string",
372
+ description: "Route name (e.g., 'get-by-id', 'create'). Creates /src/routes/<namespace>/<routeName>/",
373
+ },
374
+ handlerType: {
375
+ type: "string",
376
+ enum: ["typed", "raw"],
377
+ description: "Handler type: 'typed' for JSON-RPC, 'raw' for Request/Response",
378
+ default: "typed",
379
+ },
380
+ inputFields: {
381
+ type: "array",
382
+ items: {
383
+ type: "object",
384
+ properties: {
385
+ name: { type: "string" },
386
+ type: { type: "string", enum: ["string", "number", "boolean", "object", "array"] },
387
+ optional: { type: "boolean", default: false },
388
+ },
389
+ },
390
+ description: "Input schema fields (for typed handlers)",
391
+ },
392
+ outputFields: {
393
+ type: "array",
394
+ items: {
395
+ type: "object",
396
+ properties: {
397
+ name: { type: "string" },
398
+ type: { type: "string", enum: ["string", "number", "boolean", "object", "array"] },
399
+ },
400
+ },
401
+ description: "Output schema fields (for typed handlers)",
402
+ },
403
+ description: {
404
+ type: "string",
405
+ description: "Description of what this route does",
406
+ },
407
+ createTest: {
408
+ type: "boolean",
409
+ description: "Create a test file alongside the handler (default: true)",
410
+ default: true,
411
+ },
412
+ },
413
+ required: ["namespace", "routeName"],
414
+ },
415
+ },
416
+ {
417
+ name: "create_model",
418
+ description:
419
+ "Create a model class in /src/models/ for business logic separation. Models receive GlobalContext for testability.",
420
+ inputSchema: {
421
+ type: "object",
422
+ properties: {
423
+ name: {
424
+ type: "string",
425
+ description: "Model name (PascalCase). Example: 'UserModel' creates /src/models/UserModel.ts",
426
+ },
427
+ methods: {
428
+ type: "array",
429
+ items: {
430
+ type: "object",
431
+ properties: {
432
+ name: { type: "string" },
433
+ params: { type: "string" },
434
+ returnType: { type: "string" },
435
+ },
436
+ },
437
+ description: "Methods to scaffold on the model",
438
+ },
439
+ },
440
+ required: ["name"],
441
+ },
442
+ },
443
+ {
444
+ name: "generate_client",
445
+ description:
446
+ "Generate a typed API client from routes. The client provides type-safe methods instead of raw fetch() calls. ALWAYS use this for frontend code.",
447
+ inputSchema: {
448
+ type: "object",
449
+ properties: {
450
+ output: {
451
+ type: "string",
452
+ description: "Output path for generated client (default: src/client/api.ts)",
453
+ default: "src/client/api.ts",
454
+ },
455
+ baseUrl: {
456
+ type: "string",
457
+ description: "Default base URL for the client (default: empty, set at runtime)",
458
+ },
459
+ },
460
+ },
461
+ },
462
+ {
463
+ name: "add_unit_test",
464
+ description:
465
+ "Create a unit test file for a model or utility. Uses Bun test runner with mock context.",
466
+ inputSchema: {
467
+ type: "object",
468
+ properties: {
469
+ targetFile: {
470
+ type: "string",
471
+ description: "Path to the file being tested (e.g., 'src/models/UserModel.ts')",
472
+ },
473
+ testCases: {
474
+ type: "array",
475
+ items: {
476
+ type: "object",
477
+ properties: {
478
+ name: { type: "string", description: "Test case name" },
479
+ method: { type: "string", description: "Method being tested" },
480
+ },
481
+ },
482
+ description: "Test cases to scaffold",
483
+ },
484
+ },
485
+ required: ["targetFile"],
486
+ },
487
+ },
488
+ {
489
+ name: "add_integration_test",
490
+ description:
491
+ "Create an integration test for a route using the test harness. Tests the full request/response cycle with real plugins.",
492
+ inputSchema: {
493
+ type: "object",
494
+ properties: {
495
+ namespace: {
496
+ type: "string",
497
+ description: "Route namespace (e.g., 'users')",
498
+ },
499
+ routeName: {
500
+ type: "string",
501
+ description: "Route name (e.g., 'create')",
502
+ },
503
+ testCases: {
504
+ type: "array",
505
+ items: {
506
+ type: "object",
507
+ properties: {
508
+ name: { type: "string", description: "Test case name" },
509
+ input: { type: "object", description: "Input data for the test" },
510
+ expectedStatus: { type: "number", description: "Expected HTTP status" },
511
+ },
512
+ },
513
+ description: "Test cases to scaffold",
514
+ },
515
+ plugins: {
516
+ type: "array",
517
+ items: { type: "string" },
518
+ description: "Plugins required for this test (e.g., ['auth', 'users'])",
519
+ },
520
+ },
521
+ required: ["namespace", "routeName"],
522
+ },
523
+ },
524
+ {
525
+ name: "add_middleware",
526
+ description:
527
+ "Add middleware to a plugin. Middleware runs before route handlers for auth, logging, rate limiting, etc.",
528
+ inputSchema: {
529
+ type: "object",
530
+ properties: {
531
+ pluginName: {
532
+ type: "string",
533
+ description: "Plugin to add middleware to",
534
+ },
535
+ middlewareName: {
536
+ type: "string",
537
+ description: "Name of the middleware (camelCase). Example: 'requireAuth'",
538
+ },
539
+ description: {
540
+ type: "string",
541
+ description: "What this middleware does",
542
+ },
543
+ modifiesContext: {
544
+ type: "boolean",
545
+ description: "Whether this middleware adds to context (e.g., ctx.user)",
546
+ default: false,
547
+ },
548
+ },
549
+ required: ["pluginName", "middlewareName"],
550
+ },
551
+ },
552
+ {
553
+ name: "add_custom_handler",
554
+ description:
555
+ "Add a custom handler type to a plugin. Custom handlers extend beyond 'typed' and 'raw' for specialized use cases.",
556
+ inputSchema: {
557
+ type: "object",
558
+ properties: {
559
+ pluginName: {
560
+ type: "string",
561
+ description: "Plugin to add the handler to",
562
+ },
563
+ handlerName: {
564
+ type: "string",
565
+ description: "Name of the handler (camelCase). Example: 'streaming'",
566
+ },
567
+ description: {
568
+ type: "string",
569
+ description: "What this handler type does",
570
+ },
571
+ signatureParams: {
572
+ type: "string",
573
+ description: "Handler function parameters. Example: 'data: T, ctx: ServerContext'",
574
+ },
575
+ signatureReturn: {
576
+ type: "string",
577
+ description: "Handler function return type. Example: 'Promise<Response>'",
578
+ },
579
+ },
580
+ required: ["pluginName", "handlerName"],
581
+ },
582
+ },
583
+ {
584
+ name: "create_custom_error",
585
+ description:
586
+ "Create a custom error type for a plugin. Custom errors extend HttpError with specific status codes and machine-readable error codes. Use for domain-specific errors like 'InsufficientFunds' or 'QuotaExceeded'.",
587
+ inputSchema: {
588
+ type: "object",
589
+ properties: {
590
+ pluginName: {
591
+ type: "string",
592
+ description: "Plugin to add the error to",
593
+ },
594
+ errorName: {
595
+ type: "string",
596
+ description: "Error name (PascalCase, no 'Error' suffix). E.g., 'InsufficientFunds'",
597
+ },
598
+ httpStatus: {
599
+ type: "number",
600
+ description: "HTTP status code (default: 400)",
601
+ default: 400,
602
+ },
603
+ defaultMessage: {
604
+ type: "string",
605
+ description: "Default error message shown to users",
606
+ },
607
+ errorCode: {
608
+ type: "string",
609
+ description: "Machine-readable code (UPPER_SNAKE_CASE). E.g., 'INSUFFICIENT_FUNDS'",
610
+ },
611
+ },
612
+ required: ["pluginName", "errorName"],
613
+ },
614
+ },
615
+ {
616
+ name: "validate_code",
617
+ description:
618
+ "Validate code against @donkeylabs/server conventions. Checks for common mistakes, missing patterns, and best practices. Use after writing code to ensure quality.",
619
+ inputSchema: {
620
+ type: "object",
621
+ properties: {
622
+ filePath: {
623
+ type: "string",
624
+ description: "Path to the file to validate (relative to project root)",
625
+ },
626
+ checkType: {
627
+ type: "string",
628
+ enum: ["plugin", "route", "model", "auto"],
629
+ description: "Type of validation: 'plugin', 'route', 'model', or 'auto' (detect from path). Default: auto",
630
+ default: "auto",
631
+ },
632
+ },
633
+ required: ["filePath"],
634
+ },
635
+ },
636
+ ];
637
+
638
+ // ============================================
639
+ // Tool Implementations
640
+ // ============================================
641
+
642
+ async function initProject(args: {
643
+ projectName?: string;
644
+ useDatabase?: boolean;
645
+ }): Promise<string> {
646
+ const { projectName = "my-server", useDatabase = true } = args;
647
+ const root = process.cwd();
648
+
649
+ const createdFiles: string[] = [];
650
+
651
+ // Create directories
652
+ await mkdir(join(root, "src/plugins"), { recursive: true });
653
+ await mkdir(join(root, "src/routes"), { recursive: true });
654
+ await mkdir(join(root, ".@donkeylabs/server"), { recursive: true });
655
+
656
+ // Create donkeylabs.config.ts
657
+ const configContent = `import { defineConfig } from "@donkeylabs/server";
658
+
659
+ export default defineConfig({
660
+ plugins: ["./src/plugins/**/index.ts"],
661
+ outDir: ".@donkeylabs/server",
662
+ routes: "./src/routes/**/handler.ts",
663
+ });
664
+ `;
665
+ await writeFile(join(root, "donkeylabs.config.ts"), configContent);
666
+ createdFiles.push("donkeylabs.config.ts");
667
+
668
+ // Create database setup if requested
669
+ if (useDatabase) {
670
+ const dbContent = `import { Kysely } from "kysely";
671
+ import { BunSqliteDialect } from "kysely-bun-sqlite";
672
+ import { Database } from "bun:sqlite";
673
+
674
+ const dbPath = process.env.DATABASE_URL || "app.db";
675
+
676
+ export const db = new Kysely<any>({
677
+ dialect: new BunSqliteDialect({
678
+ database: new Database(dbPath),
679
+ }),
680
+ });
681
+
682
+ export type DB = typeof db;
683
+ `;
684
+ await writeFile(join(root, "src/db.ts"), dbContent);
685
+ createdFiles.push("src/db.ts");
686
+ }
687
+
688
+ // Create main index.ts
689
+ const indexContent = useDatabase
690
+ ? `import { db } from "./db";
691
+ import { AppServer, createRouter } from "@donkeylabs/server";
692
+ import { z } from "zod";
693
+
694
+ // Create Server
695
+ const server = new AppServer({
696
+ port: Number(process.env.PORT) || 3000,
697
+ db,
698
+ config: { env: process.env.NODE_ENV || "development" },
699
+ });
700
+
701
+ // Health check route
702
+ const router = createRouter("api")
703
+ .route("health").raw({
704
+ handle: async () => {
705
+ return Response.json({ status: "ok", timestamp: new Date().toISOString() });
706
+ },
707
+ });
708
+
709
+ server.use(router);
710
+
711
+ // Start Server
712
+ await server.start();
713
+ console.log("Server running on http://localhost:3000");
714
+ `
715
+ : `import { AppServer, createRouter } from "@donkeylabs/server";
716
+ import { Kysely, DummyDriver, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
717
+ import { z } from "zod";
718
+
719
+ // Dummy database (no persistence)
720
+ const db = new Kysely<any>({
721
+ dialect: {
722
+ createAdapter: () => new SqliteAdapter(),
723
+ createDriver: () => new DummyDriver(),
724
+ createIntrospector: (db) => new SqliteIntrospector(db),
725
+ createQueryCompiler: () => new SqliteQueryCompiler(),
726
+ },
727
+ });
728
+
729
+ // Create Server
730
+ const server = new AppServer({
731
+ port: Number(process.env.PORT) || 3000,
732
+ db,
733
+ config: { env: process.env.NODE_ENV || "development" },
734
+ });
735
+
736
+ // Health check route
737
+ const router = createRouter("api")
738
+ .route("health").raw({
739
+ handle: async () => {
740
+ return Response.json({ status: "ok", timestamp: new Date().toISOString() });
741
+ },
742
+ });
743
+
744
+ server.use(router);
745
+
746
+ // Start Server
747
+ await server.start();
748
+ console.log("Server running on http://localhost:3000");
749
+ `;
750
+ await writeFile(join(root, "src/index.ts"), indexContent);
751
+ createdFiles.push("src/index.ts");
752
+
753
+ // Create/update package.json
754
+ const pkgPath = join(root, "package.json");
755
+ let pkg: any = { name: projectName, version: "1.0.0", type: "module", scripts: {} };
756
+
757
+ if (existsSync(pkgPath)) {
758
+ try {
759
+ pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
760
+ pkg.scripts = pkg.scripts || {};
761
+ } catch {}
762
+ }
763
+
764
+ pkg.name = pkg.name || projectName;
765
+ pkg.type = "module";
766
+ pkg.scripts.dev = "bun --watch src/index.ts";
767
+ pkg.scripts.start = "bun src/index.ts";
768
+ pkg.scripts["gen:types"] = "donkeylabs generate";
769
+
770
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\\n");
771
+ createdFiles.push("package.json");
772
+
773
+ // Create tsconfig.json
774
+ const tsconfigContent = `{
775
+ "compilerOptions": {
776
+ "lib": ["ESNext"],
777
+ "target": "ESNext",
778
+ "module": "Preserve",
779
+ "moduleDetection": "force",
780
+ "jsx": "react-jsx",
781
+ "allowJs": true,
782
+ "moduleResolution": "bundler",
783
+ "allowImportingTsExtensions": true,
784
+ "verbatimModuleSyntax": true,
785
+ "noEmit": true,
786
+ "strict": true,
787
+ "skipLibCheck": true
788
+ },
789
+ "include": ["src/**/*", "*.ts", ".@donkeylabs/**/*"]
790
+ }
791
+ `;
792
+ if (!existsSync(join(root, "tsconfig.json"))) {
793
+ await writeFile(join(root, "tsconfig.json"), tsconfigContent);
794
+ createdFiles.push("tsconfig.json");
795
+ }
796
+
797
+ // Create .gitignore
798
+ const gitignoreContent = `node_modules/
799
+ .@donkeylabs/
800
+ *.db
801
+ *.db-journal
802
+ .env
803
+ .env.local
804
+ `;
805
+ if (!existsSync(join(root, ".gitignore"))) {
806
+ await writeFile(join(root, ".gitignore"), gitignoreContent);
807
+ createdFiles.push(".gitignore");
808
+ }
809
+
810
+ return `Project initialized successfully!
811
+
812
+ Created files:
813
+ ${createdFiles.map((f) => " - " + f).join("\\n")}
814
+
815
+ Next steps:
816
+ 1. Install dependencies: bun install @donkeylabs/server kysely kysely-bun-sqlite zod
817
+ 2. Create a plugin: use create_plugin tool
818
+ 3. Add routes: use add_route_file tool
819
+ 4. Generate types: use generate_types tool
820
+ 5. Start server: bun run dev`;
821
+ }
822
+
823
+ async function findProjectRoot(): Promise<string> {
824
+ let dir = process.cwd();
825
+ while (dir !== "/") {
826
+ if (existsSync(join(dir, "donkeylabs.config.ts")) || existsSync(join(dir, "package.json"))) {
827
+ return dir;
828
+ }
829
+ dir = join(dir, "..");
830
+ }
831
+ return process.cwd();
832
+ }
833
+
834
+ async function getPluginsDir(): Promise<string> {
835
+ const root = await findProjectRoot();
836
+ // Check for src/plugins (user project) or examples/basic-server/src/plugins (dev)
837
+ const srcPlugins = join(root, "src/plugins");
838
+ if (existsSync(srcPlugins)) return srcPlugins;
839
+ return join(root, "src/plugins"); // Default to creating here
840
+ }
841
+
842
+ async function createPlugin(args: {
843
+ name: string;
844
+ hasSchema?: boolean;
845
+ dependencies?: string[];
846
+ serviceMethods?: string[];
847
+ }): Promise<string> {
848
+ const { name, hasSchema = false, dependencies = [], serviceMethods = [] } = args;
849
+
850
+ // Validate name
851
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
852
+ throw new Error("Plugin name must be lowercase alphanumeric with dashes, starting with a letter");
853
+ }
854
+
855
+ const pluginsDir = await getPluginsDir();
856
+ const pluginDir = join(pluginsDir, name);
857
+
858
+ if (existsSync(pluginDir)) {
859
+ throw new Error(`Plugin '${name}' already exists at ${pluginDir}`);
860
+ }
861
+
862
+ await mkdir(pluginDir, { recursive: true });
863
+
864
+ // Generate names
865
+ const camelName = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
866
+ const pascalName = camelName.charAt(0).toUpperCase() + camelName.slice(1);
867
+
868
+ // Generate service interface
869
+ const serviceInterface = serviceMethods.length > 0
870
+ ? serviceMethods.map((m) => ` ${m}(): Promise<void>;`).join("\n")
871
+ : " getData(): Promise<string>;";
872
+
873
+ // Generate service implementation
874
+ const serviceImpl = serviceMethods.length > 0
875
+ ? serviceMethods
876
+ .map(
877
+ (m) => ` ${m}: async () => {
878
+ // TODO: Implement ${m}
879
+ },`
880
+ )
881
+ .join("\n")
882
+ : ` getData: async () => {
883
+ return "Hello from ${name} plugin!";
884
+ },`;
885
+
886
+ // Generate dependencies line
887
+ const depsLine = dependencies.length > 0
888
+ ? ` dependencies: [${dependencies.map((d) => `"${d}"`).join(", ")}] as const,\n`
889
+ : "";
890
+
891
+ // Generate deps access comments
892
+ const depsComments = dependencies.length > 0
893
+ ? dependencies.map((d) => ` // Access ${d}: ctx.deps.${d}`).join("\n") + "\n"
894
+ : "";
895
+
896
+ const pluginContent = `import { createPlugin } from "@donkeylabs/server";
897
+ ${hasSchema ? `import type { DB as ${pascalName}Schema } from "./schema";\n` : ""}
898
+ export interface ${pascalName}Service {
899
+ ${serviceInterface}
900
+ }
901
+
902
+ export const ${camelName}Plugin = createPlugin${hasSchema ? `\n .withSchema<${pascalName}Schema>()` : ""}
903
+ .define({
904
+ name: "${name}",
905
+ version: "1.0.0",
906
+ ${depsLine} service: async (ctx): Promise<${pascalName}Service> => {
907
+ // Available core services:
908
+ // - ctx.core.logger.info("message", { data }) - Structured logging
909
+ // - ctx.core.cache.set("key", value, { ttl: 60 }) - Caching with TTL
910
+ // - ctx.core.events.emit("event", data) - Pub/sub events
911
+ // - ctx.core.jobs.enqueue("job", payload) - Background jobs
912
+ // - ctx.core.cron.schedule("0 * * * *", handler) - Scheduled tasks
913
+ // - ctx.core.sse.broadcast("channel", data) - Real-time updates
914
+ // - ctx.core.rateLimiter.limit("key") - Rate limiting
915
+ //
916
+ // Error factories (throw these):
917
+ // - ctx.errors.BadRequest("message") - 400
918
+ // - ctx.errors.Unauthorized("message") - 401
919
+ // - ctx.errors.Forbidden("message") - 403
920
+ // - ctx.errors.NotFound("message") - 404
921
+ // - ctx.errors.Conflict("message") - 409
922
+ // - ctx.errors.TooManyRequests("message") - 429
923
+ ${depsComments}
924
+ return {
925
+ ${serviceImpl}
926
+ };
927
+ },
928
+ });
929
+ `;
930
+
931
+ await writeFile(join(pluginDir, "index.ts"), pluginContent);
932
+
933
+ const createdFiles = [`src/plugins/${name}/index.ts`];
934
+
935
+ if (hasSchema) {
936
+ const schemaContent = `// Database schema types for ${name} plugin
937
+ // Run type generation after adding migrations
938
+
939
+ export interface DB {
940
+ // Define your table interfaces here
941
+ // ${name}: {
942
+ // id: Generated<number>;
943
+ // name: string;
944
+ // created_at: Generated<string>;
945
+ // };
946
+ }
947
+ `;
948
+ await writeFile(join(pluginDir, "schema.ts"), schemaContent);
949
+ createdFiles.push(`src/plugins/${name}/schema.ts`);
950
+
951
+ await mkdir(join(pluginDir, "migrations"), { recursive: true });
952
+
953
+ const migrationContent = `import { Kysely, sql } from "kysely";
954
+
955
+ export async function up(db: Kysely<any>): Promise<void> {
956
+ // Create your tables here
957
+ // await db.schema
958
+ // .createTable("${name}")
959
+ // .ifNotExists()
960
+ // .addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
961
+ // .addColumn("created_at", "text", (col) => col.defaultTo(sql\`CURRENT_TIMESTAMP\`))
962
+ // .execute();
963
+ }
964
+
965
+ export async function down(db: Kysely<any>): Promise<void> {
966
+ // await db.schema.dropTable("${name}").ifExists().execute();
967
+ }
968
+ `;
969
+ await writeFile(join(pluginDir, "migrations", "001_initial.ts"), migrationContent);
970
+ createdFiles.push(`src/plugins/${name}/migrations/001_initial.ts`);
971
+ }
972
+
973
+ return `Created plugin '${name}' with files:\n${createdFiles.map((f) => ` - ${f}`).join("\n")}\n\nNext steps:\n1. Implement your service methods\n${hasSchema ? "2. Define your database schema in migrations\n3. " : "2. "}Run 'donkeylabs generate' to update types`;
974
+ }
975
+
976
+ async function addMigration(args: {
977
+ pluginName: string;
978
+ migrationName: string;
979
+ upSql?: string;
980
+ downSql?: string;
981
+ }): Promise<string> {
982
+ const { pluginName, migrationName, upSql, downSql } = args;
983
+
984
+ if (!/^[a-z0-9_]+$/.test(migrationName)) {
985
+ throw new Error("Migration name must be snake_case (lowercase with underscores)");
986
+ }
987
+
988
+ const pluginsDir = await getPluginsDir();
989
+ const migrationsDir = join(pluginsDir, pluginName, "migrations");
990
+
991
+ if (!existsSync(join(pluginsDir, pluginName))) {
992
+ throw new Error(`Plugin '${pluginName}' not found`);
993
+ }
994
+
995
+ await mkdir(migrationsDir, { recursive: true });
996
+
997
+ // Get next migration number
998
+ let nextNum = 1;
999
+ try {
1000
+ const files = await readdir(migrationsDir);
1001
+ const nums = files
1002
+ .map((f) => parseInt(f.split("_")[0] || "", 10))
1003
+ .filter((n) => !isNaN(n));
1004
+ if (nums.length > 0) {
1005
+ nextNum = Math.max(...nums) + 1;
1006
+ }
1007
+ } catch {}
1008
+
1009
+ const filename = `${String(nextNum).padStart(3, "0")}_${migrationName}.ts`;
1010
+
1011
+ const content = `import { Kysely, sql } from "kysely";
1012
+
1013
+ export async function up(db: Kysely<any>): Promise<void> {
1014
+ ${upSql || "// Add your migration code here"}
1015
+ }
1016
+
1017
+ export async function down(db: Kysely<any>): Promise<void> {
1018
+ ${downSql || "// Add your rollback code here"}
1019
+ }
1020
+ `;
1021
+
1022
+ await writeFile(join(migrationsDir, filename), content);
1023
+
1024
+ return `Created migration: src/plugins/${pluginName}/migrations/${filename}`;
1025
+ }
1026
+
1027
+ async function listPlugins(): Promise<string> {
1028
+ const pluginsDir = await getPluginsDir();
1029
+
1030
+ if (!existsSync(pluginsDir)) {
1031
+ return "No plugins directory found. Run 'donkeylabs init' to set up a project.";
1032
+ }
1033
+
1034
+ const entries = await readdir(pluginsDir, { withFileTypes: true });
1035
+ const plugins: string[] = [];
1036
+
1037
+ for (const entry of entries) {
1038
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
1039
+ const indexPath = join(pluginsDir, entry.name, "index.ts");
1040
+ if (existsSync(indexPath)) {
1041
+ const content = await readFile(indexPath, "utf-8");
1042
+
1043
+ // Extract dependencies
1044
+ const depsMatch = content.match(/dependencies:\s*\[([^\]]*)\]/);
1045
+ const deps = depsMatch?.[1]
1046
+ ? depsMatch[1].match(/"([^"]+)"/g)?.map((d) => d.replace(/"/g, "")) || []
1047
+ : [];
1048
+
1049
+ // Extract service methods
1050
+ const serviceMatch = content.match(/interface\s+\w+Service\s*\{([^}]+)\}/);
1051
+ const methods = serviceMatch?.[1]
1052
+ ? [...serviceMatch[1].matchAll(/(\w+)\s*\(/g)].map((m) => m[1])
1053
+ : [];
1054
+
1055
+ // Check for schema
1056
+ const hasSchema = existsSync(join(pluginsDir, entry.name, "schema.ts"));
1057
+
1058
+ plugins.push(
1059
+ `${entry.name}${hasSchema ? " [has schema]" : ""}\n` +
1060
+ (deps.length > 0 ? ` Dependencies: ${deps.join(", ")}\n` : "") +
1061
+ (methods.length > 0 ? ` Methods: ${methods.join(", ")}` : "")
1062
+ );
1063
+ }
1064
+ }
1065
+ }
1066
+
1067
+ if (plugins.length === 0) {
1068
+ return "No plugins found.";
1069
+ }
1070
+
1071
+ return `Found ${plugins.length} plugin(s):\n\n${plugins.join("\n\n")}`;
1072
+ }
1073
+
1074
+ async function getProjectInfo(): Promise<string> {
1075
+ const root = await findProjectRoot();
1076
+
1077
+ const info: string[] = [`Project root: ${root}`];
1078
+
1079
+ // Check for config
1080
+ const configPath = join(root, "donkeylabs.config.ts");
1081
+ if (existsSync(configPath)) {
1082
+ info.push("Config: donkeylabs.config.ts found");
1083
+ } else {
1084
+ info.push("Config: Not found (run 'donkeylabs init')");
1085
+ }
1086
+
1087
+ // Check package.json
1088
+ const pkgPath = join(root, "package.json");
1089
+ if (existsSync(pkgPath)) {
1090
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
1091
+ info.push(`Package: ${pkg.name}@${pkg.version || "0.0.0"}`);
1092
+ }
1093
+
1094
+ // List plugins
1095
+ const pluginsDir = join(root, "src/plugins");
1096
+ if (existsSync(pluginsDir)) {
1097
+ const entries = await readdir(pluginsDir, { withFileTypes: true });
1098
+ const pluginNames = entries
1099
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
1100
+ .map((e) => e.name);
1101
+ info.push(`Plugins: ${pluginNames.length > 0 ? pluginNames.join(", ") : "none"}`);
1102
+ }
1103
+
1104
+ // Available handlers
1105
+ info.push("\nBuilt-in handlers: typed, raw");
1106
+
1107
+ // Structure reminder
1108
+ info.push(`
1109
+ Project structure:
1110
+ src/
1111
+ index.ts # Server entry point
1112
+ plugins/ # Plugin modules
1113
+ <name>/
1114
+ index.ts # Plugin definition
1115
+ schema.ts # DB types (if needed)
1116
+ migrations/ # SQL migrations
1117
+ donkeylabs.config.ts`);
1118
+
1119
+ return info.join("\n");
1120
+ }
1121
+
1122
+ async function generateTypes(): Promise<string> {
1123
+ const root = await findProjectRoot();
1124
+
1125
+ // Try to run the generate command
1126
+ const proc = Bun.spawn(["bun", "cli/index.ts", "generate"], {
1127
+ cwd: root,
1128
+ stdout: "pipe",
1129
+ stderr: "pipe",
1130
+ });
1131
+
1132
+ const output = await new Response(proc.stdout).text();
1133
+ const error = await new Response(proc.stderr).text();
1134
+ await proc.exited;
1135
+
1136
+ if (proc.exitCode !== 0) {
1137
+ return `Type generation failed:\n${error || output}`;
1138
+ }
1139
+
1140
+ return `Types generated successfully!\n${output}`;
1141
+ }
1142
+
1143
+ async function addRoute(args: {
1144
+ routerFile: string;
1145
+ routeName: string;
1146
+ handlerType?: string;
1147
+ inputFields?: { name: string; type: string; optional?: boolean }[];
1148
+ outputFields?: { name: string; type: string }[];
1149
+ description?: string;
1150
+ }): Promise<string> {
1151
+ const {
1152
+ routerFile,
1153
+ routeName,
1154
+ handlerType = "typed",
1155
+ inputFields = [],
1156
+ outputFields = [],
1157
+ description,
1158
+ } = args;
1159
+
1160
+ const root = await findProjectRoot();
1161
+ const filePath = join(root, routerFile);
1162
+
1163
+ if (!existsSync(filePath)) {
1164
+ // Provide helpful guidance if file doesn't exist
1165
+ const suggestion = routerFile.includes("routes/")
1166
+ ? `Use 'create_router' tool first to create the router file.`
1167
+ : `Check that the file path is correct. For a new router, use 'create_router' tool.`;
1168
+ throw new Error(`File not found: ${routerFile}\n\n${suggestion}`);
1169
+ }
1170
+
1171
+ const content = await readFile(filePath, "utf-8");
1172
+
1173
+ // Generate Zod schema for input
1174
+ const inputSchema =
1175
+ inputFields.length > 0
1176
+ ? `z.object({\n ${inputFields
1177
+ .map((f) => `${f.name}: z.${f.type}()${f.optional ? ".optional()" : ""}`)
1178
+ .join(",\n ")}\n })`
1179
+ : "z.object({})";
1180
+
1181
+ // Generate Zod schema for output
1182
+ const outputSchema =
1183
+ outputFields.length > 0
1184
+ ? `z.object({\n ${outputFields.map((f) => `${f.name}: z.${f.type}()`).join(",\n ")}\n })`
1185
+ : "z.object({ success: z.boolean() })";
1186
+
1187
+ // Generate route code
1188
+ const routeCode =
1189
+ handlerType === "typed"
1190
+ ? `
1191
+ .route("${routeName}").typed({${description ? `\n // ${description}` : ""}
1192
+ input: ${inputSchema},
1193
+ output: ${outputSchema},
1194
+ handle: async (input, ctx) => {
1195
+ // TODO: Implement ${routeName}
1196
+ return { success: true };
1197
+ },
1198
+ })`
1199
+ : `
1200
+ .route("${routeName}").raw({${description ? `\n // ${description}` : ""}
1201
+ handle: async (req, ctx) => {
1202
+ // TODO: Implement ${routeName}
1203
+ return new Response("OK");
1204
+ },
1205
+ })`;
1206
+
1207
+ // Find where to insert (before server.use or at end of router chain)
1208
+ const insertPoint = content.lastIndexOf("server.use(");
1209
+ if (insertPoint === -1) {
1210
+ throw new Error("Could not find insertion point. Make sure the file has a router.");
1211
+ }
1212
+
1213
+ const newContent = content.slice(0, insertPoint) + routeCode + "\n\n" + content.slice(insertPoint);
1214
+
1215
+ await writeFile(filePath, newContent);
1216
+
1217
+ return `Added route '${routeName}' to ${routerFile}\n\nRemember to implement the handler logic!`;
1218
+ }
1219
+
1220
+ async function addServiceMethod(args: {
1221
+ pluginName: string;
1222
+ methodName: string;
1223
+ params?: { name: string; type: string }[];
1224
+ returnType?: string;
1225
+ implementation?: string;
1226
+ }): Promise<string> {
1227
+ const {
1228
+ pluginName,
1229
+ methodName,
1230
+ params = [],
1231
+ returnType = "Promise<void>",
1232
+ implementation,
1233
+ } = args;
1234
+
1235
+ const pluginsDir = await getPluginsDir();
1236
+ const indexPath = join(pluginsDir, pluginName, "index.ts");
1237
+
1238
+ if (!existsSync(indexPath)) {
1239
+ throw new Error(`Plugin '${pluginName}' not found`);
1240
+ }
1241
+
1242
+ let content = await readFile(indexPath, "utf-8");
1243
+
1244
+ // Generate param string
1245
+ const paramStr = params.map((p) => `${p.name}: ${p.type}`).join(", ");
1246
+
1247
+ // Add to interface
1248
+ const interfaceMatch = content.match(/(interface\s+\w+Service\s*\{)([^}]*)(})/);
1249
+ if (!interfaceMatch) {
1250
+ throw new Error("Could not find service interface in plugin");
1251
+ }
1252
+
1253
+ const newInterfaceMethod = `\n ${methodName}(${paramStr}): ${returnType};`;
1254
+ content = content.replace(
1255
+ interfaceMatch[0],
1256
+ interfaceMatch[1] + interfaceMatch[2] + newInterfaceMethod + "\n" + interfaceMatch[3]
1257
+ );
1258
+
1259
+ // Add to implementation
1260
+ const returnMatch = content.match(/(return\s*\{)([^}]*)(};?\s*},?\s*}\s*\);?\s*$)/s);
1261
+ if (!returnMatch) {
1262
+ throw new Error("Could not find service implementation in plugin");
1263
+ }
1264
+
1265
+ const impl = implementation || `// TODO: Implement ${methodName}`;
1266
+ const newImplMethod = `\n ${methodName}: async (${paramStr}) => {\n ${impl}\n },`;
1267
+ content = content.replace(
1268
+ returnMatch[0],
1269
+ returnMatch[1] + returnMatch[2] + newImplMethod + "\n " + returnMatch[3]
1270
+ );
1271
+
1272
+ await writeFile(indexPath, content);
1273
+
1274
+ return `Added method '${methodName}' to ${pluginName} plugin`;
1275
+ }
1276
+
1277
+ async function createRouter(args: {
1278
+ fileName: string;
1279
+ namespace: string;
1280
+ initialRoutes?: string[];
1281
+ }): Promise<string> {
1282
+ const { fileName, namespace, initialRoutes = [] } = args;
1283
+
1284
+ const root = await findProjectRoot();
1285
+ const routesDir = join(root, "src/routes");
1286
+ await mkdir(routesDir, { recursive: true });
1287
+
1288
+ const filePath = join(routesDir, `${fileName}.ts`);
1289
+
1290
+ if (existsSync(filePath)) {
1291
+ throw new Error(`Router file already exists: src/routes/${fileName}.ts`);
1292
+ }
1293
+
1294
+ // Generate initial routes
1295
+ const routeDefinitions = initialRoutes.length > 0
1296
+ ? initialRoutes.map((route) => `
1297
+ .route("${route}").typed({
1298
+ input: z.object({}),
1299
+ output: z.object({ success: z.boolean() }),
1300
+ handle: async (input, ctx) => {
1301
+ // TODO: Implement ${route}
1302
+ return { success: true };
1303
+ },
1304
+ })`).join("")
1305
+ : `
1306
+ .route("hello").typed({
1307
+ input: z.object({ name: z.string().optional() }),
1308
+ output: z.object({ message: z.string() }),
1309
+ handle: async (input) => {
1310
+ return { message: \`Hello from ${namespace}!\` };
1311
+ },
1312
+ })`;
1313
+
1314
+ const content = `import { createRouter } from "@donkeylabs/server";
1315
+ import { z } from "zod";
1316
+
1317
+ export const ${fileName}Router = createRouter("${namespace}")${routeDefinitions};
1318
+ `;
1319
+
1320
+ await writeFile(filePath, content);
1321
+
1322
+ return `Created router at src/routes/${fileName}.ts
1323
+
1324
+ To use this router, add to your src/index.ts:
1325
+
1326
+ import { ${fileName}Router } from "./routes/${fileName}";
1327
+ // ... after server.registerPlugin calls
1328
+ server.use(${fileName}Router);`;
1329
+ }
1330
+
1331
+ async function validateProject(): Promise<string> {
1332
+ const root = await findProjectRoot();
1333
+ const issues: string[] = [];
1334
+ const warnings: string[] = [];
1335
+
1336
+ // Check for config file
1337
+ if (!existsSync(join(root, "donkeylabs.config.ts"))) {
1338
+ issues.push("Missing donkeylabs.config.ts - run 'donkeylabs init'");
1339
+ }
1340
+
1341
+ // Check for package.json
1342
+ const pkgPath = join(root, "package.json");
1343
+ if (existsSync(pkgPath)) {
1344
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
1345
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1346
+
1347
+ // Check required dependencies
1348
+ if (!deps["@donkeylabs/server"]) {
1349
+ issues.push("Missing dependency: @donkeylabs/server");
1350
+ }
1351
+ if (!deps["kysely"]) {
1352
+ issues.push("Missing dependency: kysely");
1353
+ }
1354
+ if (!deps["zod"]) {
1355
+ issues.push("Missing dependency: zod");
1356
+ }
1357
+ } else {
1358
+ issues.push("Missing package.json");
1359
+ }
1360
+
1361
+ // Check for src/index.ts
1362
+ const indexPath = join(root, "src/index.ts");
1363
+ if (existsSync(indexPath)) {
1364
+ const indexContent = await readFile(indexPath, "utf-8");
1365
+
1366
+ // Check for DummyDriver (bad)
1367
+ if (indexContent.includes("DummyDriver")) {
1368
+ warnings.push("src/index.ts uses DummyDriver - this is a placeholder, not a real database");
1369
+ }
1370
+
1371
+ // Check for AppServer
1372
+ if (!indexContent.includes("AppServer")) {
1373
+ warnings.push("src/index.ts doesn't import AppServer");
1374
+ }
1375
+ } else {
1376
+ issues.push("Missing src/index.ts - run 'donkeylabs init'");
1377
+ }
1378
+
1379
+ // Check plugins directory
1380
+ const pluginsDir = join(root, "src/plugins");
1381
+ if (existsSync(pluginsDir)) {
1382
+ const entries = await readdir(pluginsDir, { withFileTypes: true });
1383
+
1384
+ for (const entry of entries) {
1385
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
1386
+ const pluginIndex = join(pluginsDir, entry.name, "index.ts");
1387
+
1388
+ if (!existsSync(pluginIndex)) {
1389
+ issues.push(`Plugin '${entry.name}' is missing index.ts`);
1390
+ continue;
1391
+ }
1392
+
1393
+ const content = await readFile(pluginIndex, "utf-8");
1394
+
1395
+ // Check for proper export
1396
+ if (!content.includes("createPlugin")) {
1397
+ warnings.push(`Plugin '${entry.name}' doesn't use createPlugin`);
1398
+ }
1399
+
1400
+ // Check if plugin is registered in index.ts
1401
+ if (existsSync(indexPath)) {
1402
+ const indexContent = await readFile(indexPath, "utf-8");
1403
+ if (!indexContent.includes(`${entry.name}Plugin`) && !indexContent.includes(`'./plugins/${entry.name}'`)) {
1404
+ warnings.push(`Plugin '${entry.name}' exists but may not be registered in src/index.ts`);
1405
+ }
1406
+ }
1407
+ }
1408
+ }
1409
+ }
1410
+
1411
+ // Check for generated types
1412
+ const genDir = join(root, ".@donkeylabs/server");
1413
+ if (!existsSync(genDir) || !existsSync(join(genDir, "registry.d.ts"))) {
1414
+ warnings.push("Generated types not found - run 'donkeylabs generate'");
1415
+ }
1416
+
1417
+ // Format output
1418
+ if (issues.length === 0 && warnings.length === 0) {
1419
+ return "Project validation passed. No issues found.";
1420
+ }
1421
+
1422
+ let output = "";
1423
+
1424
+ if (issues.length > 0) {
1425
+ output += `ERRORS (${issues.length}):\n${issues.map((i) => ` - ${i}`).join("\n")}\n\n`;
1426
+ }
1427
+
1428
+ if (warnings.length > 0) {
1429
+ output += `WARNINGS (${warnings.length}):\n${warnings.map((w) => ` - ${w}`).join("\n")}`;
1430
+ }
1431
+
1432
+ return output;
1433
+ }
1434
+
1435
+ async function registerPlugin(args: {
1436
+ pluginName: string;
1437
+ configOptions?: Record<string, any>;
1438
+ }): Promise<string> {
1439
+ const { pluginName, configOptions } = args;
1440
+
1441
+ const root = await findProjectRoot();
1442
+ const pluginsDir = join(root, "src/plugins");
1443
+ const indexPath = join(root, "src/index.ts");
1444
+
1445
+ // Verify plugin exists
1446
+ const pluginDir = join(pluginsDir, pluginName);
1447
+ if (!existsSync(pluginDir)) {
1448
+ throw new Error(`Plugin '${pluginName}' not found in src/plugins/`);
1449
+ }
1450
+
1451
+ // Read plugin to get export name
1452
+ const pluginIndexPath = join(pluginDir, "index.ts");
1453
+ const pluginContent = await readFile(pluginIndexPath, "utf-8");
1454
+ const exportMatch = pluginContent.match(/export\s+const\s+(\w+Plugin)\s*=/);
1455
+ const exportName = exportMatch?.[1];
1456
+
1457
+ if (!exportName) {
1458
+ throw new Error(`Could not find plugin export in ${pluginName}/index.ts`);
1459
+ }
1460
+
1461
+ // Read index.ts
1462
+ if (!existsSync(indexPath)) {
1463
+ throw new Error("src/index.ts not found");
1464
+ }
1465
+
1466
+ let content = await readFile(indexPath, "utf-8");
1467
+
1468
+ // Check if already imported
1469
+ if (content.includes(`from "./plugins/${pluginName}"`) || content.includes(`from './plugins/${pluginName}'`)) {
1470
+ return `Plugin '${pluginName}' is already imported in src/index.ts`;
1471
+ }
1472
+
1473
+ // Add import after other plugin imports or after @donkeylabs/server import
1474
+ const importLine = `import { ${exportName} } from "./plugins/${pluginName}";\n`;
1475
+
1476
+ // Find a good place to add the import
1477
+ const lastPluginImport = content.lastIndexOf('from "./plugins/');
1478
+ const serverImport = content.indexOf('from "@donkeylabs/server"');
1479
+
1480
+ if (lastPluginImport !== -1) {
1481
+ // Add after last plugin import
1482
+ const lineEnd = content.indexOf("\n", lastPluginImport);
1483
+ content = content.slice(0, lineEnd + 1) + importLine + content.slice(lineEnd + 1);
1484
+ } else if (serverImport !== -1) {
1485
+ // Add after server import
1486
+ const lineEnd = content.indexOf("\n", serverImport);
1487
+ content = content.slice(0, lineEnd + 1) + importLine + content.slice(lineEnd + 1);
1488
+ } else {
1489
+ // Add at top
1490
+ content = importLine + content;
1491
+ }
1492
+
1493
+ // Add registerPlugin call
1494
+ const configStr = configOptions ? JSON.stringify(configOptions, null, 2) : "";
1495
+ const registerCall = configStr
1496
+ ? `server.registerPlugin(${exportName}(${configStr}));\n`
1497
+ : `server.registerPlugin(${exportName});\n`;
1498
+
1499
+ // Find where to add registration (after other registerPlugin calls or after new AppServer)
1500
+ const lastRegister = content.lastIndexOf("server.registerPlugin(");
1501
+ const serverCreate = content.indexOf("new AppServer(");
1502
+
1503
+ if (lastRegister !== -1) {
1504
+ const lineEnd = content.indexOf("\n", lastRegister);
1505
+ content = content.slice(0, lineEnd + 1) + registerCall + content.slice(lineEnd + 1);
1506
+ } else if (serverCreate !== -1) {
1507
+ // Find the end of the AppServer construction (closing brace and semicolon)
1508
+ let braceCount = 0;
1509
+ let pos = serverCreate;
1510
+ while (pos < content.length) {
1511
+ if (content[pos] === "{") braceCount++;
1512
+ if (content[pos] === "}") braceCount--;
1513
+ if (braceCount === 0 && content[pos] === ";") {
1514
+ content = content.slice(0, pos + 1) + "\n\n" + registerCall + content.slice(pos + 1);
1515
+ break;
1516
+ }
1517
+ pos++;
1518
+ }
1519
+ } else {
1520
+ throw new Error("Could not find place to add registerPlugin call");
1521
+ }
1522
+
1523
+ await writeFile(indexPath, content);
1524
+
1525
+ return `Registered plugin '${pluginName}' in src/index.ts\n\nAdded:\n - import { ${exportName} } from "./plugins/${pluginName}"\n - server.registerPlugin(${exportName}${configStr ? "(...)" : ""})`;
1526
+ }
1527
+
1528
+ async function runMigrations(args: {
1529
+ pluginName?: string;
1530
+ direction?: string;
1531
+ }): Promise<string> {
1532
+ const { pluginName, direction = "up" } = args;
1533
+
1534
+ const root = await findProjectRoot();
1535
+ const pluginsDir = join(root, "src/plugins");
1536
+
1537
+ if (!existsSync(pluginsDir)) {
1538
+ throw new Error("No plugins directory found");
1539
+ }
1540
+
1541
+ const results: string[] = [];
1542
+
1543
+ // Get list of plugins to migrate
1544
+ const pluginsToMigrate: string[] = [];
1545
+ if (pluginName) {
1546
+ if (!existsSync(join(pluginsDir, pluginName))) {
1547
+ throw new Error(`Plugin '${pluginName}' not found`);
1548
+ }
1549
+ pluginsToMigrate.push(pluginName);
1550
+ } else {
1551
+ const entries = await readdir(pluginsDir, { withFileTypes: true });
1552
+ for (const entry of entries) {
1553
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
1554
+ pluginsToMigrate.push(entry.name);
1555
+ }
1556
+ }
1557
+ }
1558
+
1559
+ for (const plugin of pluginsToMigrate) {
1560
+ const migrationsDir = join(pluginsDir, plugin, "migrations");
1561
+
1562
+ if (!existsSync(migrationsDir)) {
1563
+ results.push(`${plugin}: No migrations directory`);
1564
+ continue;
1565
+ }
1566
+
1567
+ const files = await readdir(migrationsDir);
1568
+ const migrationFiles = files
1569
+ .filter((f) => f.endsWith(".ts") && !f.startsWith("."))
1570
+ .sort();
1571
+
1572
+ if (migrationFiles.length === 0) {
1573
+ results.push(`${plugin}: No migrations found`);
1574
+ continue;
1575
+ }
1576
+
1577
+ // If going down, reverse the order
1578
+ if (direction === "down") {
1579
+ migrationFiles.reverse();
1580
+ }
1581
+
1582
+ results.push(`${plugin}: Found ${migrationFiles.length} migration(s)`);
1583
+
1584
+ for (const file of migrationFiles) {
1585
+ const migrationPath = join(migrationsDir, file);
1586
+ try {
1587
+ const migration = await import(migrationPath);
1588
+
1589
+ if (direction === "up" && typeof migration.up === "function") {
1590
+ results.push(` - ${file}: Would run 'up' migration`);
1591
+ } else if (direction === "down" && typeof migration.down === "function") {
1592
+ results.push(` - ${file}: Would run 'down' migration`);
1593
+ } else {
1594
+ results.push(` - ${file}: No '${direction}' function found`);
1595
+ }
1596
+ } catch (err: any) {
1597
+ results.push(` - ${file}: Error loading - ${err.message}`);
1598
+ }
1599
+ }
1600
+ }
1601
+
1602
+ results.push("");
1603
+ results.push("Note: This tool shows what migrations are available.");
1604
+ results.push("To actually run migrations, execute them in your server code:");
1605
+ results.push(" await migration.up(db); // or migration.down(db)");
1606
+
1607
+ return results.join("\n");
1608
+ }
1609
+
1610
+ async function setupDatabase(args: {
1611
+ dbPath?: string;
1612
+ }): Promise<string> {
1613
+ const { dbPath = "app.db" } = args;
1614
+ const root = await findProjectRoot();
1615
+ const fullPath = join(root, dbPath);
1616
+
1617
+ const results: string[] = [];
1618
+
1619
+ // Check if src/db.ts exists
1620
+ const dbSetupPath = join(root, "src/db.ts");
1621
+ if (!existsSync(dbSetupPath)) {
1622
+ results.push("WARNING: src/db.ts not found");
1623
+ results.push("Run 'donkeylabs init' with SQLite enabled to create database setup.");
1624
+ return results.join("\n");
1625
+ }
1626
+
1627
+ results.push(`Database setup file: src/db.ts`);
1628
+
1629
+ // Check if database file exists
1630
+ if (existsSync(fullPath)) {
1631
+ results.push(`Database file exists: ${dbPath}`);
1632
+
1633
+ // Try to get some info about the database
1634
+ try {
1635
+ const { Database } = await import("bun:sqlite");
1636
+ const db = new Database(fullPath, { readonly: true });
1637
+ const tables = db.query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all() as { name: string }[];
1638
+ db.close();
1639
+
1640
+ if (tables.length > 0) {
1641
+ results.push(`Tables: ${tables.map((t) => t.name).join(", ")}`);
1642
+ } else {
1643
+ results.push("No tables yet - run migrations to create them");
1644
+ }
1645
+ } catch (err: any) {
1646
+ results.push(`Could not inspect database: ${err.message}`);
1647
+ }
1648
+ } else {
1649
+ results.push(`Database file will be created at: ${dbPath}`);
1650
+ results.push("The database is created automatically when your server starts.");
1651
+ }
1652
+
1653
+ results.push("");
1654
+ results.push("Database setup looks good!");
1655
+
1656
+ return results.join("\n");
1657
+ }
1658
+
1659
+ async function listRoutes(args: { namespace?: string }): Promise<string> {
1660
+ const root = await findProjectRoot();
1661
+ const routesDir = join(root, "src/routes");
1662
+
1663
+ if (!existsSync(routesDir)) {
1664
+ return "No routes directory found. Routes should be in src/routes/<namespace>/<route>/handler.ts";
1665
+ }
1666
+
1667
+ const results: string[] = ["Routes found:\n"];
1668
+ let routeCount = 0;
1669
+
1670
+ const namespaces = await readdir(routesDir, { withFileTypes: true });
1671
+
1672
+ for (const ns of namespaces) {
1673
+ if (!ns.isDirectory() || ns.name.startsWith(".")) continue;
1674
+ if (args.namespace && ns.name !== args.namespace) continue;
1675
+
1676
+ const nsDir = join(routesDir, ns.name);
1677
+ const routeDirs = await readdir(nsDir, { withFileTypes: true });
1678
+
1679
+ results.push(`${ns.name}/`);
1680
+
1681
+ for (const routeDir of routeDirs) {
1682
+ if (!routeDir.isDirectory() || routeDir.name.startsWith(".")) continue;
1683
+
1684
+ const handlerPath = join(nsDir, routeDir.name, "handler.ts");
1685
+ const testPath = join(nsDir, routeDir.name, "test.ts");
1686
+
1687
+ if (!existsSync(handlerPath)) continue;
1688
+
1689
+ const content = await readFile(handlerPath, "utf-8");
1690
+ const handlerType = content.includes(".raw(") ? "raw" : "typed";
1691
+ const hasTest = existsSync(testPath);
1692
+
1693
+ results.push(` ${routeDir.name}/ (${handlerType})${hasTest ? " [tested]" : ""}`);
1694
+ routeCount++;
1695
+ }
1696
+ }
1697
+
1698
+ if (routeCount === 0) {
1699
+ return "No routes found. Use 'add_route_file' to create routes.";
1700
+ }
1701
+
1702
+ results.push(`\nTotal: ${routeCount} route(s)`);
1703
+ return results.join("\n");
1704
+ }
1705
+
1706
+ async function addRouteFile(args: {
1707
+ namespace: string;
1708
+ routeName: string;
1709
+ handlerType?: string;
1710
+ inputFields?: { name: string; type: string; optional?: boolean }[];
1711
+ outputFields?: { name: string; type: string }[];
1712
+ description?: string;
1713
+ createTest?: boolean;
1714
+ }): Promise<string> {
1715
+ const {
1716
+ namespace,
1717
+ routeName,
1718
+ handlerType = "typed",
1719
+ inputFields = [],
1720
+ outputFields = [],
1721
+ description,
1722
+ createTest = true,
1723
+ } = args;
1724
+
1725
+ const root = await findProjectRoot();
1726
+ const routeDir = join(root, "src/routes", namespace, routeName);
1727
+
1728
+ if (existsSync(routeDir)) {
1729
+ throw new Error(`Route already exists: src/routes/${namespace}/${routeName}/`);
1730
+ }
1731
+
1732
+ await mkdir(routeDir, { recursive: true });
1733
+
1734
+ // Generate input schema
1735
+ const inputSchema = inputFields.length > 0
1736
+ ? `z.object({\n ${inputFields.map((f) => `${f.name}: z.${f.type}()${f.optional ? ".optional()" : ""}`).join(",\n ")}\n })`
1737
+ : "z.object({})";
1738
+
1739
+ // Generate output schema
1740
+ const outputSchema = outputFields.length > 0
1741
+ ? `z.object({\n ${outputFields.map((f) => `${f.name}: z.${f.type}()`).join(",\n ")}\n })`
1742
+ : "z.object({ success: z.boolean() })";
1743
+
1744
+ // Generate handler file
1745
+ const handlerContent = handlerType === "typed"
1746
+ ? `/**
1747
+ * ${namespace}.${routeName}
1748
+ * ${description || "TODO: Add description"}
1749
+ */
1750
+ import { z } from "zod";
1751
+ import type { GlobalContext } from "../../../.@donkeylabs/server/context";
1752
+
1753
+ export const input = ${inputSchema};
1754
+ export const output = ${outputSchema};
1755
+
1756
+ export async function handler(
1757
+ data: z.infer<typeof input>,
1758
+ ctx: GlobalContext
1759
+ ): Promise<z.infer<typeof output>> {
1760
+ // Available in ctx:
1761
+ // - ctx.db: Database queries (ctx.db.selectFrom("table")...)
1762
+ // - ctx.plugins: Plugin services (ctx.plugins.auth, ctx.plugins.users, etc.)
1763
+ // - ctx.core.logger: Logging (ctx.core.logger.info("message", { data }))
1764
+ // - ctx.core.cache: Caching (ctx.core.cache.get("key"), ctx.core.cache.set("key", value))
1765
+ // - ctx.errors: Error factories (throw ctx.errors.NotFound("User not found"))
1766
+ // - ctx.user: Authenticated user (if auth middleware applied)
1767
+
1768
+ // TODO: Implement handler logic
1769
+ return { success: true };
1770
+ }
1771
+ `
1772
+ : `/**
1773
+ * ${namespace}.${routeName}
1774
+ * ${description || "TODO: Add description"}
1775
+ */
1776
+ import type { GlobalContext } from "../../../.@donkeylabs/server/context";
1777
+
1778
+ export async function handler(
1779
+ req: Request,
1780
+ ctx: GlobalContext
1781
+ ): Promise<Response> {
1782
+ // Available in ctx:
1783
+ // - ctx.db: Database queries
1784
+ // - ctx.plugins: Plugin services (ctx.plugins.auth, etc.)
1785
+ // - ctx.core.logger/cache/events/jobs/cron/sse/rateLimiter
1786
+ // - ctx.errors: Error factories
1787
+ // - ctx.user: Authenticated user (if auth middleware applied)
1788
+
1789
+ // TODO: Implement handler logic
1790
+ return new Response("OK");
1791
+ }
1792
+ `;
1793
+
1794
+ await writeFile(join(routeDir, "handler.ts"), handlerContent);
1795
+
1796
+ const createdFiles = [`src/routes/${namespace}/${routeName}/handler.ts`];
1797
+
1798
+ // Generate test file
1799
+ if (createTest) {
1800
+ const testContent = handlerType === "typed"
1801
+ ? `import { describe, test, expect } from "bun:test";
1802
+ import { handler, input, output } from "./handler";
1803
+
1804
+ describe("${namespace}.${routeName}", () => {
1805
+ test("should return success", async () => {
1806
+ // TODO: Create mock context
1807
+ const mockCtx = {} as any;
1808
+ const result = await handler({}, mockCtx);
1809
+ expect(result.success).toBe(true);
1810
+ });
1811
+
1812
+ test("should validate input", () => {
1813
+ expect(() => input.parse({})).not.toThrow();
1814
+ });
1815
+ });
1816
+ `
1817
+ : `import { describe, test, expect } from "bun:test";
1818
+ import { handler } from "./handler";
1819
+
1820
+ describe("${namespace}.${routeName}", () => {
1821
+ test("should return OK", async () => {
1822
+ const mockCtx = {} as any;
1823
+ const req = new Request("http://localhost/${namespace}.${routeName}");
1824
+ const response = await handler(req, mockCtx);
1825
+ expect(response.status).toBe(200);
1826
+ });
1827
+ });
1828
+ `;
1829
+
1830
+ await writeFile(join(routeDir, "test.ts"), testContent);
1831
+ createdFiles.push(`src/routes/${namespace}/${routeName}/test.ts`);
1832
+ }
1833
+
1834
+ return `Created route ${namespace}.${routeName}:\n${createdFiles.map((f) => ` - ${f}`).join("\n")}\n\nRun 'donkeylabs generate' to update types.`;
1835
+ }
1836
+
1837
+ async function createModel(args: {
1838
+ name: string;
1839
+ methods?: { name: string; params: string; returnType: string }[];
1840
+ }): Promise<string> {
1841
+ const { name, methods = [] } = args;
1842
+
1843
+ const root = await findProjectRoot();
1844
+ const modelsDir = join(root, "src/models");
1845
+ await mkdir(modelsDir, { recursive: true });
1846
+
1847
+ const filePath = join(modelsDir, `${name}.ts`);
1848
+
1849
+ if (existsSync(filePath)) {
1850
+ throw new Error(`Model already exists: src/models/${name}.ts`);
1851
+ }
1852
+
1853
+ // Generate method stubs
1854
+ const methodStubs = methods.length > 0
1855
+ ? methods.map((m) => `
1856
+ async ${m.name}(${m.params}): ${m.returnType} {
1857
+ // TODO: Implement ${m.name}
1858
+ throw new Error("Not implemented");
1859
+ }`).join("\n")
1860
+ : `
1861
+ async getData(): Promise<any> {
1862
+ // TODO: Implement getData
1863
+ return null;
1864
+ }`;
1865
+
1866
+ const content = `/**
1867
+ * ${name}
1868
+ *
1869
+ * Business logic model with dependency injection for testability.
1870
+ */
1871
+ import type { GlobalContext } from "../.@donkeylabs/server/context";
1872
+
1873
+ export class ${name} {
1874
+ constructor(private ctx: GlobalContext) {}
1875
+ ${methodStubs}
1876
+ }
1877
+
1878
+ /**
1879
+ * Factory function for creating ${name} instances.
1880
+ * Use this in route handlers for clean dependency injection.
1881
+ */
1882
+ export function create${name}(ctx: GlobalContext): ${name} {
1883
+ return new ${name}(ctx);
1884
+ }
1885
+ `;
1886
+
1887
+ await writeFile(filePath, content);
1888
+
1889
+ return `Created model: src/models/${name}.ts
1890
+
1891
+ Usage in route handler:
1892
+ import { create${name} } from "../../models/${name}";
1893
+
1894
+ export async function handler(data, ctx) {
1895
+ const model = create${name}(ctx);
1896
+ return model.getData();
1897
+ }`;
1898
+ }
1899
+
1900
+ async function addUnitTest(args: {
1901
+ targetFile: string;
1902
+ testCases?: { name: string; method: string }[];
1903
+ }): Promise<string> {
1904
+ const { targetFile, testCases = [] } = args;
1905
+
1906
+ const root = await findProjectRoot();
1907
+ const fullPath = join(root, targetFile);
1908
+
1909
+ if (!existsSync(fullPath)) {
1910
+ throw new Error(`File not found: ${targetFile}`);
1911
+ }
1912
+
1913
+ // Determine test file path (sibling .test.ts file)
1914
+ const testPath = targetFile.replace(/\.ts$/, ".test.ts");
1915
+ const fullTestPath = join(root, testPath);
1916
+
1917
+ // Read target file to extract exports
1918
+ const content = await readFile(fullPath, "utf-8");
1919
+ const classMatch = content.match(/export\s+class\s+(\w+)/);
1920
+ const functionMatch = content.match(/export\s+function\s+(\w+)/);
1921
+
1922
+ const exportName = classMatch?.[1] || functionMatch?.[1] || "module";
1923
+ const isClass = !!classMatch;
1924
+
1925
+ // Generate test cases
1926
+ const testCaseCode = testCases.length > 0
1927
+ ? testCases.map((tc) => `
1928
+ test("${tc.name}", async () => {
1929
+ ${isClass ? `// const instance = new ${exportName}(mockCtx);` : ""}
1930
+ // const result = ${isClass ? "instance" : exportName}.${tc.method}();
1931
+ // expect(result).toBeDefined();
1932
+ expect(true).toBe(true); // TODO: Implement test
1933
+ });`).join("\n")
1934
+ : `
1935
+ test("should work correctly", async () => {
1936
+ // TODO: Implement test
1937
+ expect(true).toBe(true);
1938
+ });`;
1939
+
1940
+ const testContent = `import { describe, test, expect, mock } from "bun:test";
1941
+ import { ${exportName} } from "./${targetFile.split("/").pop()?.replace(/\.ts$/, "")}";
1942
+
1943
+ describe("${exportName}", () => {
1944
+ // Mock GlobalContext for unit testing
1945
+ const mockCtx = {
1946
+ db: {} as any,
1947
+ plugins: {} as any,
1948
+ core: {} as any,
1949
+ errors: {} as any,
1950
+ ip: "127.0.0.1",
1951
+ };
1952
+ ${testCaseCode}
1953
+ });
1954
+ `;
1955
+
1956
+ await writeFile(fullTestPath, testContent);
1957
+
1958
+ return `Created unit test: ${testPath}
1959
+
1960
+ Run tests with: bun test ${testPath}`;
1961
+ }
1962
+
1963
+ async function addIntegrationTest(args: {
1964
+ namespace: string;
1965
+ routeName: string;
1966
+ testCases?: { name: string; input?: any; expectedStatus?: number }[];
1967
+ plugins?: string[];
1968
+ }): Promise<string> {
1969
+ const { namespace, routeName, testCases = [], plugins = [] } = args;
1970
+
1971
+ const root = await findProjectRoot();
1972
+ const testDir = join(root, "test/integration");
1973
+ await mkdir(testDir, { recursive: true });
1974
+
1975
+ const testPath = join(testDir, `${namespace}.${routeName}.test.ts`);
1976
+
1977
+ // Generate plugin imports
1978
+ const pluginImports = plugins.length > 0
1979
+ ? plugins.map((p) => `import { ${p}Plugin } from "../src/plugins/${p}";`).join("\n")
1980
+ : "// Import plugins as needed";
1981
+
1982
+ const pluginList = plugins.length > 0
1983
+ ? plugins.map((p) => `${p}Plugin`).join(", ")
1984
+ : "/* plugins */";
1985
+
1986
+ // Generate test cases
1987
+ const testCaseCode = testCases.length > 0
1988
+ ? testCases.map((tc) => `
1989
+ test("${tc.name}", async () => {
1990
+ const response = await harness.request("${namespace}.${routeName}", ${JSON.stringify(tc.input || {})});
1991
+ expect(response.status).toBe(${tc.expectedStatus || 200});
1992
+ });`).join("\n")
1993
+ : `
1994
+ test("should handle request successfully", async () => {
1995
+ const response = await harness.request("${namespace}.${routeName}", {});
1996
+ expect(response.status).toBe(200);
1997
+ });
1998
+
1999
+ test("should handle invalid input", async () => {
2000
+ // TODO: Test with invalid input
2001
+ expect(true).toBe(true);
2002
+ });`;
2003
+
2004
+ const content = `import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2005
+ import { createTestHarness } from "@donkeylabs/server/harness";
2006
+ ${pluginImports}
2007
+
2008
+ describe("${namespace}.${routeName} (Integration)", () => {
2009
+ let harness: Awaited<ReturnType<typeof createTestHarness>>;
2010
+
2011
+ beforeAll(async () => {
2012
+ harness = await createTestHarness(${pluginList});
2013
+ await harness.manager.initialize();
2014
+ });
2015
+
2016
+ afterAll(async () => {
2017
+ // Cleanup if needed
2018
+ });
2019
+ ${testCaseCode}
2020
+ });
2021
+ `;
2022
+
2023
+ await writeFile(testPath, content);
2024
+
2025
+ return `Created integration test: test/integration/${namespace}.${routeName}.test.ts
2026
+
2027
+ Run tests with: bun test test/integration/${namespace}.${routeName}.test.ts`;
2028
+ }
2029
+
2030
+ async function generateClientTool(args: {
2031
+ output?: string;
2032
+ baseUrl?: string;
2033
+ }): Promise<string> {
2034
+ const { output = "src/client/api.ts", baseUrl = "" } = args;
2035
+
2036
+ const root = await findProjectRoot();
2037
+ const routesDir = join(root, "src/routes");
2038
+ const outputPath = join(root, output);
2039
+
2040
+ // Collect routes
2041
+ interface RouteInfo {
2042
+ namespace: string;
2043
+ name: string;
2044
+ handlerType: "typed" | "raw";
2045
+ }
2046
+
2047
+ const routes: RouteInfo[] = [];
2048
+
2049
+ if (existsSync(routesDir)) {
2050
+ const namespaces = await readdir(routesDir, { withFileTypes: true });
2051
+
2052
+ for (const ns of namespaces) {
2053
+ if (!ns.isDirectory() || ns.name.startsWith(".")) continue;
2054
+
2055
+ const nsDir = join(routesDir, ns.name);
2056
+ const routeDirs = await readdir(nsDir, { withFileTypes: true });
2057
+
2058
+ for (const routeDir of routeDirs) {
2059
+ if (!routeDir.isDirectory() || routeDir.name.startsWith(".")) continue;
2060
+
2061
+ const handlerPath = join(nsDir, routeDir.name, "handler.ts");
2062
+ if (!existsSync(handlerPath)) continue;
2063
+
2064
+ const content = await readFile(handlerPath, "utf-8");
2065
+ const handlerType = content.includes(".raw(") || content.includes("req: Request") ? "raw" : "typed";
2066
+
2067
+ routes.push({
2068
+ namespace: ns.name,
2069
+ name: routeDir.name,
2070
+ handlerType,
2071
+ });
2072
+ }
2073
+ }
2074
+ }
2075
+
2076
+ if (routes.length === 0) {
2077
+ return "No routes found. Create routes first with 'add_route_file' tool.";
2078
+ }
2079
+
2080
+ // Group by namespace
2081
+ const byNamespace = new Map<string, RouteInfo[]>();
2082
+ for (const route of routes) {
2083
+ if (!byNamespace.has(route.namespace)) {
2084
+ byNamespace.set(route.namespace, []);
2085
+ }
2086
+ byNamespace.get(route.namespace)!.push(route);
2087
+ }
2088
+
2089
+ // Generate namespace methods
2090
+ const namespaceBlocks: string[] = [];
2091
+
2092
+ for (const [namespace, nsRoutes] of byNamespace) {
2093
+ const methods = nsRoutes.map((r) => {
2094
+ const methodName = r.name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2095
+ if (r.handlerType === "typed") {
2096
+ return ` /** Call ${namespace}.${r.name} */
2097
+ ${methodName}: <TInput, TOutput>(input: TInput): Promise<TOutput> =>
2098
+ this.request("${namespace}.${r.name}", input)`;
2099
+ } else {
2100
+ return ` /** Call ${namespace}.${r.name} (raw) */
2101
+ ${methodName}: (init?: RequestInit): Promise<Response> =>
2102
+ this.rawRequest("${namespace}.${r.name}", init)`;
2103
+ }
2104
+ });
2105
+
2106
+ namespaceBlocks.push(` ${namespace} = {\n${methods.join(",\n\n")}\n };`);
2107
+ }
2108
+
2109
+ const content = `/**
2110
+ * Auto-generated API Client
2111
+ * Generated by: donkeylabs generate_client
2112
+ *
2113
+ * Usage:
2114
+ * import { api } from "./client/api";
2115
+ * const result = await api.users.create({ email: "test@example.com" });
2116
+ *
2117
+ * DO NOT use raw fetch() - use this typed client instead!
2118
+ */
2119
+
2120
+ export interface ApiClientOptions {
2121
+ baseUrl: string;
2122
+ credentials?: RequestCredentials;
2123
+ headers?: HeadersInit;
2124
+ }
2125
+
2126
+ export class ApiClient {
2127
+ constructor(private options: ApiClientOptions) {}
2128
+
2129
+ private async request<TInput, TOutput>(
2130
+ route: string,
2131
+ input: TInput
2132
+ ): Promise<TOutput> {
2133
+ const response = await fetch(\`\${this.options.baseUrl}/\${route}\`, {
2134
+ method: "POST",
2135
+ headers: {
2136
+ "Content-Type": "application/json",
2137
+ ...this.options.headers,
2138
+ },
2139
+ credentials: this.options.credentials || "include",
2140
+ body: JSON.stringify(input),
2141
+ });
2142
+
2143
+ if (!response.ok) {
2144
+ const error = await response.json().catch(() => ({ message: response.statusText }));
2145
+ throw new ApiError(response.status, error.message || "Request failed", error);
2146
+ }
2147
+
2148
+ return response.json();
2149
+ }
2150
+
2151
+ private async rawRequest(route: string, init?: RequestInit): Promise<Response> {
2152
+ return fetch(\`\${this.options.baseUrl}/\${route}\`, {
2153
+ credentials: this.options.credentials || "include",
2154
+ ...init,
2155
+ });
2156
+ }
2157
+
2158
+ ${namespaceBlocks.join("\n\n")}
2159
+ }
2160
+
2161
+ export class ApiError extends Error {
2162
+ constructor(
2163
+ public status: number,
2164
+ message: string,
2165
+ public data?: any
2166
+ ) {
2167
+ super(message);
2168
+ this.name = "ApiError";
2169
+ }
2170
+ }
2171
+
2172
+ /**
2173
+ * Create API client instance
2174
+ *
2175
+ * @example
2176
+ * const api = createApiClient({ baseUrl: "http://localhost:3000" });
2177
+ * const user = await api.users.create({ email: "test@example.com" });
2178
+ */
2179
+ export function createApiClient(options: ApiClientOptions): ApiClient {
2180
+ return new ApiClient(options);
2181
+ }
2182
+
2183
+ ${baseUrl ? `// Pre-configured client instance\nexport const api = createApiClient({ baseUrl: "${baseUrl}" });\n` : "// Configure and export: export const api = createApiClient({ baseUrl: 'http://localhost:3000' });"}
2184
+ `;
2185
+
2186
+ await mkdir(join(root, "src/client"), { recursive: true });
2187
+ await writeFile(outputPath, content);
2188
+
2189
+ return `Generated API client: ${output}
2190
+
2191
+ Routes included:
2192
+ ${routes.map((r) => ` - ${r.namespace}.${r.name} (${r.handlerType})`).join("\n")}
2193
+
2194
+ Usage:
2195
+ import { createApiClient } from "./${output.replace(/\.ts$/, "")}";
2196
+
2197
+ const api = createApiClient({ baseUrl: "http://localhost:3000" });
2198
+
2199
+ // Type-safe API calls (NOT raw fetch!)
2200
+ const result = await api.${routes[0]?.namespace || "namespace"}.${routes[0]?.name.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) || "method"}({ ... });`;
2201
+ }
2202
+
2203
+ async function addMiddleware(args: {
2204
+ pluginName: string;
2205
+ middlewareName: string;
2206
+ description?: string;
2207
+ modifiesContext?: boolean;
2208
+ }): Promise<string> {
2209
+ const { pluginName, middlewareName, description, modifiesContext = false } = args;
2210
+
2211
+ const pluginsDir = await getPluginsDir();
2212
+ const indexPath = join(pluginsDir, pluginName, "index.ts");
2213
+
2214
+ if (!existsSync(indexPath)) {
2215
+ throw new Error(`Plugin '${pluginName}' not found`);
2216
+ }
2217
+
2218
+ let content = await readFile(indexPath, "utf-8");
2219
+
2220
+ // Check if middleware already exists
2221
+ if (content.includes(`${middlewareName}:`)) {
2222
+ throw new Error(`Middleware '${middlewareName}' already exists in plugin`);
2223
+ }
2224
+
2225
+ // Create middleware definition
2226
+ const middlewareCode = `
2227
+ // ${description || `${middlewareName} middleware`}
2228
+ export const ${middlewareName}Middleware = createMiddleware({
2229
+ name: "${middlewareName}",
2230
+ handler: async (ctx, next) => {
2231
+ // TODO: Implement middleware logic
2232
+ ${modifiesContext ? "// Example: ctx.user = await validateToken(ctx);" : ""}
2233
+ return next();
2234
+ },
2235
+ });
2236
+ `;
2237
+
2238
+ // Check if createMiddleware is imported
2239
+ if (!content.includes("createMiddleware")) {
2240
+ content = content.replace(
2241
+ /from "@donkeylabs\/server"/,
2242
+ 'from "@donkeylabs/server";\nimport { createMiddleware } from "@donkeylabs/server"'
2243
+ );
2244
+ }
2245
+
2246
+ // Add to plugin definition's middleware object
2247
+ const middlewareMatch = content.match(/middleware:\s*\{([^}]*)\}/);
2248
+ if (middlewareMatch) {
2249
+ // Add to existing middleware object
2250
+ const insertPos = middlewareMatch.index! + middlewareMatch[0].length - 1;
2251
+ const existingMiddleware = middlewareMatch[1].trim();
2252
+ const separator = existingMiddleware ? ",\n " : "\n ";
2253
+ content = content.slice(0, insertPos) + separator + `${middlewareName}: ${middlewareName}Middleware` + content.slice(insertPos);
2254
+ } else {
2255
+ // Add middleware object to plugin definition
2256
+ const defineMatch = content.match(/\.define\s*\(\s*\{/);
2257
+ if (defineMatch) {
2258
+ const insertPos = defineMatch.index! + defineMatch[0].length;
2259
+ content = content.slice(0, insertPos) + `\n middleware: {\n ${middlewareName}: ${middlewareName}Middleware,\n },` + content.slice(insertPos);
2260
+ }
2261
+ }
2262
+
2263
+ // Add middleware definition before the plugin export
2264
+ const exportMatch = content.match(/export const \w+Plugin/);
2265
+ if (exportMatch) {
2266
+ content = content.slice(0, exportMatch.index) + middlewareCode + "\n" + content.slice(exportMatch.index);
2267
+ }
2268
+
2269
+ await writeFile(indexPath, content);
2270
+
2271
+ return `Added middleware '${middlewareName}' to ${pluginName} plugin.
2272
+
2273
+ Usage in routes:
2274
+ router.route("protected").${middlewareName}().typed({ ... })
2275
+
2276
+ Remember to run 'donkeylabs generate' to update types!`;
2277
+ }
2278
+
2279
+ async function addCustomHandler(args: {
2280
+ pluginName: string;
2281
+ handlerName: string;
2282
+ description?: string;
2283
+ signatureParams?: string;
2284
+ signatureReturn?: string;
2285
+ }): Promise<string> {
2286
+ const {
2287
+ pluginName,
2288
+ handlerName,
2289
+ description,
2290
+ signatureParams = "data: any, ctx: ServerContext",
2291
+ signatureReturn = "Promise<any>",
2292
+ } = args;
2293
+
2294
+ const pluginsDir = await getPluginsDir();
2295
+ const indexPath = join(pluginsDir, pluginName, "index.ts");
2296
+
2297
+ if (!existsSync(indexPath)) {
2298
+ throw new Error(`Plugin '${pluginName}' not found`);
2299
+ }
2300
+
2301
+ let content = await readFile(indexPath, "utf-8");
2302
+
2303
+ // Check if handler already exists
2304
+ if (content.includes(`${handlerName}:`)) {
2305
+ throw new Error(`Handler '${handlerName}' already exists in plugin`);
2306
+ }
2307
+
2308
+ // Create handler type and definition
2309
+ const handlerCode = `
2310
+ // ${description || `${handlerName} handler`}
2311
+ type ${handlerName.charAt(0).toUpperCase() + handlerName.slice(1)}Fn = (${signatureParams}) => ${signatureReturn};
2312
+
2313
+ export const ${handlerName.charAt(0).toUpperCase() + handlerName.slice(1)}Handler = createHandler<${handlerName.charAt(0).toUpperCase() + handlerName.slice(1)}Fn>(
2314
+ async (req, def, handle, ctx) => {
2315
+ // TODO: Implement handler wrapper
2316
+ const data = await req.json();
2317
+ const result = await handle(data, ctx);
2318
+ return Response.json(result);
2319
+ }
2320
+ );
2321
+ `;
2322
+
2323
+ // Check if createHandler is imported
2324
+ if (!content.includes("createHandler")) {
2325
+ content = content.replace(
2326
+ /from "@donkeylabs\/server"/,
2327
+ ', createHandler } from "@donkeylabs/server"'
2328
+ );
2329
+ content = content.replace("{ createPlugin, createHandler }", "{ createPlugin, createHandler }");
2330
+ }
2331
+
2332
+ // Add to plugin definition's handlers object
2333
+ const handlersMatch = content.match(/handlers:\s*\{([^}]*)\}/);
2334
+ if (handlersMatch) {
2335
+ // Add to existing handlers object
2336
+ const insertPos = handlersMatch.index! + handlersMatch[0].length - 1;
2337
+ const existingHandlers = handlersMatch[1].trim();
2338
+ const separator = existingHandlers ? ",\n " : "\n ";
2339
+ content = content.slice(0, insertPos) + separator + `${handlerName}: ${handlerName.charAt(0).toUpperCase() + handlerName.slice(1)}Handler` + content.slice(insertPos);
2340
+ } else {
2341
+ // Add handlers object to plugin definition
2342
+ const defineMatch = content.match(/\.define\s*\(\s*\{/);
2343
+ if (defineMatch) {
2344
+ const insertPos = defineMatch.index! + defineMatch[0].length;
2345
+ content = content.slice(0, insertPos) + `\n handlers: {\n ${handlerName}: ${handlerName.charAt(0).toUpperCase() + handlerName.slice(1)}Handler,\n },` + content.slice(insertPos);
2346
+ }
2347
+ }
2348
+
2349
+ // Add handler definition before the plugin export
2350
+ const exportMatch = content.match(/export const \w+Plugin/);
2351
+ if (exportMatch) {
2352
+ content = content.slice(0, exportMatch.index) + handlerCode + "\n" + content.slice(exportMatch.index);
2353
+ }
2354
+
2355
+ await writeFile(indexPath, content);
2356
+
2357
+ return `Added custom handler '${handlerName}' to ${pluginName} plugin.
2358
+
2359
+ Usage in routes:
2360
+ router.route("data").${handlerName}({
2361
+ handle: async (${signatureParams.split(",")[0]}) => {
2362
+ // Return type: ${signatureReturn}
2363
+ }
2364
+ })
2365
+
2366
+ Remember to run 'donkeylabs generate' to update types!`;
2367
+ }
2368
+
2369
+ async function createCustomError(args: {
2370
+ pluginName: string;
2371
+ errorName: string;
2372
+ httpStatus?: number;
2373
+ defaultMessage?: string;
2374
+ errorCode?: string;
2375
+ }): Promise<string> {
2376
+ const {
2377
+ pluginName,
2378
+ errorName,
2379
+ httpStatus = 400,
2380
+ defaultMessage,
2381
+ errorCode,
2382
+ } = args;
2383
+
2384
+ const pluginsDir = await getPluginsDir();
2385
+ const pluginDir = join(pluginsDir, pluginName);
2386
+
2387
+ if (!existsSync(pluginDir)) {
2388
+ throw new Error(`Plugin '${pluginName}' not found`);
2389
+ }
2390
+
2391
+ // Generate error code from name if not provided
2392
+ const code = errorCode || errorName.replace(/([A-Z])/g, "_$1").toUpperCase().slice(1);
2393
+ const message = defaultMessage || errorName.replace(/([A-Z])/g, " $1").trim();
2394
+ const className = `${errorName}Error`;
2395
+
2396
+ const errorsPath = join(pluginDir, "errors.ts");
2397
+ let content: string;
2398
+
2399
+ if (existsSync(errorsPath)) {
2400
+ // Append to existing errors file
2401
+ const existing = await readFile(errorsPath, "utf-8");
2402
+
2403
+ if (existing.includes(`class ${className}`)) {
2404
+ throw new Error(`Error '${className}' already exists in ${pluginName}/errors.ts`);
2405
+ }
2406
+
2407
+ const newError = `
2408
+ export class ${className} extends HttpError {
2409
+ constructor(message = "${message}") {
2410
+ super(${httpStatus}, message, "${code}");
2411
+ this.name = "${className}";
2412
+ }
2413
+ }
2414
+ `;
2415
+ content = existing + newError;
2416
+ } else {
2417
+ // Create new errors file
2418
+ content = `import { HttpError } from "@donkeylabs/server";
2419
+
2420
+ export class ${className} extends HttpError {
2421
+ constructor(message = "${message}") {
2422
+ super(${httpStatus}, message, "${code}");
2423
+ this.name = "${className}";
2424
+ }
2425
+ }
2426
+ `;
2427
+ }
2428
+
2429
+ await writeFile(errorsPath, content);
2430
+
2431
+ return `Created custom error '${className}' in ${pluginName}/errors.ts
2432
+
2433
+ Usage:
2434
+ import { ${className} } from "./errors";
2435
+
2436
+ // In service or handler:
2437
+ throw new ${className}();
2438
+ // or with custom message:
2439
+ throw new ${className}("Custom message here");
2440
+
2441
+ HTTP Response:
2442
+ Status: ${httpStatus}
2443
+ Body: { "error": "${code}", "message": "${message}" }`;
2444
+ }
2445
+
2446
+ async function validateCode(args: {
2447
+ filePath: string;
2448
+ checkType?: string;
2449
+ }): Promise<string> {
2450
+ const { filePath, checkType = "auto" } = args;
2451
+
2452
+ const root = await findProjectRoot();
2453
+ const fullPath = join(root, filePath);
2454
+
2455
+ if (!existsSync(fullPath)) {
2456
+ throw new Error(`File not found: ${filePath}`);
2457
+ }
2458
+
2459
+ const content = await readFile(fullPath, "utf-8");
2460
+ const issues: string[] = [];
2461
+ const warnings: string[] = [];
2462
+ const suggestions: string[] = [];
2463
+
2464
+ // Detect check type from path if auto
2465
+ let type = checkType;
2466
+ if (type === "auto") {
2467
+ if (filePath.includes("/plugins/")) type = "plugin";
2468
+ else if (filePath.includes("/routes/")) type = "route";
2469
+ else if (filePath.includes("/models/")) type = "model";
2470
+ else type = "general";
2471
+ }
2472
+
2473
+ // Common checks for all files
2474
+ if (content.includes("fetch(") && !filePath.includes("client")) {
2475
+ warnings.push("Uses raw fetch() - consider using generated API client instead");
2476
+ }
2477
+
2478
+ if (content.includes("console.log(") && !content.includes("// debug")) {
2479
+ warnings.push("Contains console.log() - use ctx.core.logger instead");
2480
+ }
2481
+
2482
+ // Plugin-specific checks
2483
+ if (type === "plugin") {
2484
+ if (!content.includes("createPlugin")) {
2485
+ issues.push("Missing createPlugin import - required for plugin definition");
2486
+ }
2487
+
2488
+ if (!content.includes("export const") || !content.includes("Plugin")) {
2489
+ issues.push("No plugin export found - export should be named <name>Plugin");
2490
+ }
2491
+
2492
+ if (content.includes("service:") && !content.includes("async (ctx)") && !content.includes("async(ctx)")) {
2493
+ warnings.push("Service function should be async and receive ctx parameter");
2494
+ }
2495
+
2496
+ if (content.includes("dependencies:") && !content.includes("as const")) {
2497
+ warnings.push("Dependencies array should use 'as const' for type safety");
2498
+ }
2499
+
2500
+ if (!content.includes("version:")) {
2501
+ suggestions.push("Consider adding 'version' field to plugin definition");
2502
+ }
2503
+ }
2504
+
2505
+ // Route-specific checks
2506
+ if (type === "route") {
2507
+ if (!content.includes("z.object") && !content.includes(".raw(")) {
2508
+ warnings.push("Typed routes should have Zod input/output schemas");
2509
+ }
2510
+
2511
+ if (!content.includes("async function handler") && !content.includes("async (") && !content.includes("handle:")) {
2512
+ issues.push("Handler function should be async");
2513
+ }
2514
+
2515
+ if (!content.includes("GlobalContext") && !content.includes("ServerContext")) {
2516
+ warnings.push("Handler should use GlobalContext or ServerContext type for ctx parameter");
2517
+ }
2518
+
2519
+ const hasInputSchema = content.includes("export const input") || content.includes("input:");
2520
+ const hasOutputSchema = content.includes("export const output") || content.includes("output:");
2521
+
2522
+ if (content.includes(".typed(") && (!hasInputSchema || !hasOutputSchema)) {
2523
+ warnings.push("Typed handlers should define both input and output schemas");
2524
+ }
2525
+ }
2526
+
2527
+ // Model-specific checks
2528
+ if (type === "model") {
2529
+ if (!content.includes("constructor(") || !content.includes("ctx")) {
2530
+ warnings.push("Model constructor should accept GlobalContext for dependency injection");
2531
+ }
2532
+
2533
+ if (!content.includes("private ctx") && !content.includes("readonly ctx")) {
2534
+ suggestions.push("Consider storing ctx as private/readonly class property");
2535
+ }
2536
+
2537
+ if (!content.includes("export function create") && !content.includes("export const create")) {
2538
+ suggestions.push("Consider adding a factory function (e.g., createUserModel) for convenience");
2539
+ }
2540
+
2541
+ if (content.includes("this.db") || content.includes("this.plugins")) {
2542
+ warnings.push("Access db/plugins via this.ctx.db and this.ctx.plugins instead");
2543
+ }
2544
+ }
2545
+
2546
+ // Format output
2547
+ const results: string[] = [`Validation results for ${filePath} (${type}):\n`];
2548
+
2549
+ if (issues.length === 0 && warnings.length === 0 && suggestions.length === 0) {
2550
+ results.push("All checks passed. Code follows conventions.");
2551
+ return results.join("\n");
2552
+ }
2553
+
2554
+ if (issues.length > 0) {
2555
+ results.push(`ERRORS (${issues.length}):`);
2556
+ issues.forEach((i) => results.push(` - ${i}`));
2557
+ results.push("");
2558
+ }
2559
+
2560
+ if (warnings.length > 0) {
2561
+ results.push(`WARNINGS (${warnings.length}):`);
2562
+ warnings.forEach((w) => results.push(` - ${w}`));
2563
+ results.push("");
2564
+ }
2565
+
2566
+ if (suggestions.length > 0) {
2567
+ results.push(`SUGGESTIONS (${suggestions.length}):`);
2568
+ suggestions.forEach((s) => results.push(` - ${s}`));
2569
+ }
2570
+
2571
+ return results.join("\n");
2572
+ }
2573
+
2574
+ // ============================================
2575
+ // Server Setup
2576
+ // ============================================
2577
+
2578
+ // ============================================
2579
+ // Resource: Project Context (like CLAUDE.md)
2580
+ // ============================================
2581
+
2582
+ const PROJECT_CONTEXT = `# @donkeylabs/server - AI Development Guide
2583
+
2584
+ ## Project Structure
2585
+
2586
+ \`\`\`
2587
+ src/
2588
+ index.ts # Server entry point
2589
+ db.ts # Database setup (Kysely + SQLite)
2590
+ routes/ # Route handlers (preferred structure)
2591
+ <namespace>/
2592
+ <route-name>/
2593
+ handler.ts # Route handler
2594
+ test.ts # Unit tests
2595
+ models/ # Business logic models
2596
+ UserModel.ts # Model with GlobalContext injection
2597
+ plugins/ # Plugin modules
2598
+ <name>/
2599
+ index.ts # Plugin definition
2600
+ schema.ts # DB types (optional)
2601
+ errors.ts # Custom errors (optional)
2602
+ migrations/ # SQL migrations
2603
+ test/
2604
+ integration/ # Integration tests
2605
+ .@donkeylabs/server/ # Generated types (gitignored)
2606
+ registry.d.ts
2607
+ context.d.ts
2608
+ routes.d.ts
2609
+ \`\`\`
2610
+
2611
+ ## GlobalContext (CRITICAL - Use This!)
2612
+
2613
+ The \`GlobalContext\` type provides access to ALL services. ALWAYS use ctx instead of creating your own instances!
2614
+
2615
+ \`\`\`typescript
2616
+ import type { GlobalContext } from "./@donkeylabs/server/context";
2617
+
2618
+ interface GlobalContext {
2619
+ db: Kysely<DatabaseSchema>; // Type-safe database
2620
+ plugins: { // Plugin services
2621
+ auth: AuthService;
2622
+ users: UsersService;
2623
+ // ... all registered plugins
2624
+ };
2625
+ core: CoreServices; // Logger, cache, events, jobs, cron, sse, rateLimiter
2626
+ errors: Errors; // Error factories
2627
+ ip: string; // Client IP address
2628
+ requestId?: string; // Request tracking ID
2629
+ user?: any; // Authenticated user (set by auth middleware)
2630
+ }
2631
+ \`\`\`
2632
+
2633
+ ## Core Services (ctx.core) - Full Reference
2634
+
2635
+ ### Logger
2636
+ \`\`\`typescript
2637
+ // Structured logging with levels
2638
+ ctx.core.logger.info("User created", { userId: "123", email: "..." });
2639
+ ctx.core.logger.warn("Rate limit approaching", { remaining: 5 });
2640
+ ctx.core.logger.error("Payment failed", { error, orderId });
2641
+ ctx.core.logger.debug("Cache hit", { key, ttl });
2642
+ \`\`\`
2643
+
2644
+ ### Cache
2645
+ \`\`\`typescript
2646
+ // Set with TTL (seconds)
2647
+ await ctx.core.cache.set("user:123", userData, { ttl: 300 });
2648
+
2649
+ // Get (returns undefined if expired/missing)
2650
+ const user = await ctx.core.cache.get("user:123");
2651
+
2652
+ // Delete
2653
+ await ctx.core.cache.delete("user:123");
2654
+
2655
+ // Check existence
2656
+ const exists = await ctx.core.cache.has("user:123");
2657
+ \`\`\`
2658
+
2659
+ ### Events (Pub/Sub)
2660
+ \`\`\`typescript
2661
+ // Emit event (fire-and-forget)
2662
+ ctx.core.events.emit("user.created", { userId: "123", email: "..." });
2663
+ ctx.core.events.emit("order.completed", { orderId, total });
2664
+
2665
+ // Subscribe to events (in plugin service)
2666
+ ctx.core.events.on("user.created", async (data) => {
2667
+ await sendWelcomeEmail(data.email);
2668
+ });
2669
+
2670
+ // One-time listener
2671
+ ctx.core.events.once("server.ready", () => console.log("Server started"));
2672
+ \`\`\`
2673
+
2674
+ ### Jobs (Background Processing)
2675
+ \`\`\`typescript
2676
+ // Register job handler (in plugin initialization)
2677
+ ctx.core.jobs.register("sendEmail", async (payload) => {
2678
+ await mailer.send(payload.to, payload.subject, payload.body);
2679
+ });
2680
+
2681
+ // Enqueue job from anywhere
2682
+ await ctx.core.jobs.enqueue("sendEmail", {
2683
+ to: "user@example.com",
2684
+ subject: "Welcome!",
2685
+ body: "Welcome to our service!",
2686
+ });
2687
+
2688
+ // Enqueue with delay
2689
+ await ctx.core.jobs.enqueue("reminder", data, { delay: 60 * 60 * 1000 }); // 1 hour
2690
+ \`\`\`
2691
+
2692
+ ### Cron (Scheduled Tasks)
2693
+ \`\`\`typescript
2694
+ // Schedule recurring task (cron expression)
2695
+ ctx.core.cron.schedule("0 0 * * *", async () => {
2696
+ // Runs daily at midnight
2697
+ await cleanupExpiredSessions();
2698
+ });
2699
+
2700
+ // Schedule with options
2701
+ ctx.core.cron.schedule("*/5 * * * *", handler, {
2702
+ name: "cleanup",
2703
+ runOnStart: false,
2704
+ });
2705
+
2706
+ // Common patterns:
2707
+ // "0 * * * *" - Every hour
2708
+ // "*/5 * * * *" - Every 5 minutes
2709
+ // "0 0 * * *" - Daily at midnight
2710
+ // "0 0 * * 0" - Weekly on Sunday
2711
+ // "0 0 1 * *" - Monthly on 1st
2712
+ \`\`\`
2713
+
2714
+ ### SSE (Server-Sent Events)
2715
+ \`\`\`typescript
2716
+ // In raw handler - create SSE connection
2717
+ const client = ctx.core.sse.addClient(userId);
2718
+ return client.response; // Return this Response directly
2719
+
2720
+ // Broadcast to all connected clients
2721
+ ctx.core.sse.broadcast("notifications", { message: "New update!" });
2722
+
2723
+ // Send to specific client
2724
+ ctx.core.sse.send(userId, "private", { data: "Personal message" });
2725
+
2726
+ // Remove client on disconnect
2727
+ ctx.core.sse.removeClient(userId);
2728
+ \`\`\`
2729
+
2730
+ ### Rate Limiter
2731
+ \`\`\`typescript
2732
+ // Check rate limit
2733
+ const result = await ctx.core.rateLimiter.limit(\`api:\${ctx.ip}\`, {
2734
+ window: "1m", // Time window
2735
+ max: 100, // Max requests in window
2736
+ });
2737
+
2738
+ if (!result.allowed) {
2739
+ throw ctx.errors.TooManyRequests(\`Try again in \${result.retryAfter}s\`);
2740
+ }
2741
+
2742
+ // Rate limit by user
2743
+ const userLimit = await ctx.core.rateLimiter.limit(\`user:\${ctx.user.id}\`, {
2744
+ window: "1h",
2745
+ max: 1000,
2746
+ });
2747
+ \`\`\`
2748
+
2749
+ ## Error Factories (ctx.errors)
2750
+
2751
+ | Factory | HTTP Status | Use Case |
2752
+ |---------|-------------|----------|
2753
+ | \`BadRequest(msg)\` | 400 | Invalid input data |
2754
+ | \`Unauthorized(msg)\` | 401 | Not logged in |
2755
+ | \`Forbidden(msg)\` | 403 | No permission |
2756
+ | \`NotFound(msg)\` | 404 | Resource doesn't exist |
2757
+ | \`MethodNotAllowed(msg)\` | 405 | Wrong HTTP method |
2758
+ | \`Conflict(msg)\` | 409 | Resource already exists |
2759
+ | \`Gone(msg)\` | 410 | Resource permanently deleted |
2760
+ | \`UnprocessableEntity(msg)\` | 422 | Semantic/business logic error |
2761
+ | \`TooManyRequests(msg)\` | 429 | Rate limit exceeded |
2762
+ | \`InternalServerError(msg)\` | 500 | Unexpected server error |
2763
+ | \`NotImplemented(msg)\` | 501 | Feature not ready |
2764
+ | \`ServiceUnavailable(msg)\` | 503 | Maintenance mode |
2765
+
2766
+ Usage:
2767
+ \`\`\`typescript
2768
+ // Throw errors in handlers
2769
+ if (!user) throw ctx.errors.NotFound("User not found");
2770
+ if (!canAccess) throw ctx.errors.Forbidden("Access denied");
2771
+ if (exists) throw ctx.errors.Conflict("Email already registered");
2772
+ \`\`\`
2773
+
2774
+ ## Custom Errors
2775
+
2776
+ Create domain-specific errors using the \`create_custom_error\` tool:
2777
+ \`\`\`typescript
2778
+ // plugins/payments/errors.ts
2779
+ import { HttpError } from "@donkeylabs/server";
2780
+
2781
+ export class InsufficientFundsError extends HttpError {
2782
+ constructor(message = "Insufficient funds") {
2783
+ super(400, message, "INSUFFICIENT_FUNDS");
2784
+ }
2785
+ }
2786
+ \`\`\`
2787
+
2788
+ ## Route Structure (Preferred)
2789
+
2790
+ Routes go in \`src/routes/<namespace>/<route-name>/handler.ts\`:
2791
+
2792
+ \`\`\`typescript
2793
+ // src/routes/users/create/handler.ts
2794
+ import { z } from "zod";
2795
+ import type { GlobalContext } from "../../../.@donkeylabs/server/context";
2796
+
2797
+ export const input = z.object({
2798
+ email: z.string().email(),
2799
+ name: z.string(),
2800
+ });
2801
+
2802
+ export const output = z.object({
2803
+ user: z.object({ id: z.string() }),
2804
+ });
2805
+
2806
+ export async function handler(
2807
+ data: z.infer<typeof input>,
2808
+ ctx: GlobalContext
2809
+ ): Promise<z.infer<typeof output>> {
2810
+ const user = await ctx.plugins.users.create(data);
2811
+ return { user };
2812
+ }
2813
+ \`\`\`
2814
+
2815
+ ## Models for Testability
2816
+
2817
+ Create models in \`src/models/\` for business logic separation:
2818
+
2819
+ \`\`\`typescript
2820
+ // src/models/UserModel.ts
2821
+ import type { GlobalContext } from "../.@donkeylabs/server/context";
2822
+
2823
+ export class UserModel {
2824
+ constructor(private ctx: GlobalContext) {}
2825
+
2826
+ async create(data: { email: string; name: string }) {
2827
+ // Access ctx.db, ctx.plugins, ctx.core, ctx.errors
2828
+ return this.ctx.db.insertInto("users")
2829
+ .values(data)
2830
+ .returningAll()
2831
+ .executeTakeFirstOrThrow();
2832
+ }
2833
+ }
2834
+
2835
+ export function createUserModel(ctx: GlobalContext) {
2836
+ return new UserModel(ctx);
2837
+ }
2838
+ \`\`\`
2839
+
2840
+ Usage in route:
2841
+ \`\`\`typescript
2842
+ import { createUserModel } from "../../models/UserModel";
2843
+
2844
+ export async function handler(data, ctx) {
2845
+ const model = createUserModel(ctx);
2846
+ return model.create(data);
2847
+ }
2848
+ \`\`\`
2849
+
2850
+ ## Testing
2851
+
2852
+ ### Unit Tests (with mock context)
2853
+ Place test files next to handlers: \`handler.ts\` + \`test.ts\`
2854
+
2855
+ \`\`\`typescript
2856
+ import { describe, test, expect } from "bun:test";
2857
+ import { handler } from "./handler";
2858
+
2859
+ describe("users.create", () => {
2860
+ const mockCtx = {
2861
+ db: { insertInto: () => ({ values: () => ({ returningAll: () => ({ executeTakeFirstOrThrow: async () => ({ id: "123" }) }) }) }) } as any,
2862
+ plugins: {} as any,
2863
+ core: {} as any,
2864
+ errors: {} as any,
2865
+ ip: "127.0.0.1",
2866
+ };
2867
+
2868
+ test("creates user", async () => {
2869
+ const result = await handler({ email: "test@example.com", name: "Test" }, mockCtx);
2870
+ expect(result.user.id).toBe("123");
2871
+ });
2872
+ });
2873
+ \`\`\`
2874
+
2875
+ ### Integration Tests (with real plugins)
2876
+ Use \`createTestHarness\` for full integration tests:
2877
+
2878
+ \`\`\`typescript
2879
+ import { describe, test, expect, beforeAll } from "bun:test";
2880
+ import { createTestHarness } from "@donkeylabs/server/harness";
2881
+ import { authPlugin } from "../src/plugins/auth";
2882
+ import { usersPlugin } from "../src/plugins/users";
2883
+
2884
+ describe("users.create (Integration)", () => {
2885
+ let harness: Awaited<ReturnType<typeof createTestHarness>>;
2886
+
2887
+ beforeAll(async () => {
2888
+ harness = await createTestHarness(authPlugin, usersPlugin);
2889
+ await harness.manager.initialize();
2890
+ });
2891
+
2892
+ test("creates user with real database", async () => {
2893
+ const response = await harness.request("users.create", {
2894
+ email: "test@example.com",
2895
+ name: "Test User",
2896
+ });
2897
+ expect(response.status).toBe(200);
2898
+ const data = await response.json();
2899
+ expect(data.user.id).toBeDefined();
2900
+ });
2901
+ });
2902
+ \`\`\`
2903
+
2904
+ ## Database
2905
+ - Uses Kysely with BunSqliteDialect for SQLite
2906
+ - Each plugin manages its own tables via migrations
2907
+ - Migrations are numbered: \`001_create_users.ts\`, \`002_add_email.ts\`
2908
+
2909
+ ## Error Handling
2910
+ Use \`ctx.errors\` factories:
2911
+ - \`ctx.errors.BadRequest(message)\`
2912
+ - \`ctx.errors.Unauthorized(message)\`
2913
+ - \`ctx.errors.Forbidden(message)\`
2914
+ - \`ctx.errors.NotFound(message)\`
2915
+ - \`ctx.errors.Conflict(message)\`
2916
+
2917
+ ## API Client (Frontend)
2918
+
2919
+ ALWAYS use the generated API client instead of raw \`fetch()\` calls!
2920
+
2921
+ Generate the client:
2922
+ \`\`\`bash
2923
+ donkeylabs generate # or use generate_client MCP tool
2924
+ \`\`\`
2925
+
2926
+ Usage:
2927
+ \`\`\`typescript
2928
+ // DON'T do this:
2929
+ const res = await fetch("http://localhost:3000/users.create", { ... });
2930
+
2931
+ // DO this instead:
2932
+ import { createApiClient } from "./client/api";
2933
+
2934
+ const api = createApiClient({ baseUrl: "http://localhost:3000" });
2935
+ const user = await api.users.create({ email: "test@example.com" });
2936
+ \`\`\`
2937
+
2938
+ ## Middleware
2939
+
2940
+ Add middleware to plugins for cross-cutting concerns (auth, logging, rate limiting):
2941
+
2942
+ \`\`\`typescript
2943
+ // In plugin index.ts
2944
+ import { createMiddleware } from "@donkeylabs/server";
2945
+
2946
+ const requireAuthMiddleware = createMiddleware({
2947
+ name: "requireAuth",
2948
+ handler: async (ctx, next) => {
2949
+ const token = ctx.headers.get("Authorization");
2950
+ if (!token) throw ctx.errors.Unauthorized("No token");
2951
+ ctx.user = await validateToken(token);
2952
+ return next();
2953
+ },
2954
+ });
2955
+
2956
+ export const authPlugin = createPlugin.define({
2957
+ name: "auth",
2958
+ middleware: {
2959
+ requireAuth: requireAuthMiddleware,
2960
+ },
2961
+ // ...
2962
+ });
2963
+ \`\`\`
2964
+
2965
+ Usage in routes:
2966
+ \`\`\`typescript
2967
+ router.route("profile")
2968
+ .requireAuth() // Apply middleware
2969
+ .typed({
2970
+ handle: async (input, ctx) => {
2971
+ return ctx.user; // User is now available
2972
+ },
2973
+ });
2974
+ \`\`\`
2975
+
2976
+ ## Custom Handlers
2977
+
2978
+ Create custom handler types beyond \`typed\` and \`raw\`:
2979
+
2980
+ \`\`\`typescript
2981
+ import { createHandler } from "@donkeylabs/server";
2982
+
2983
+ type StreamingFn = (data: any, ctx: ServerContext) => AsyncGenerator<string>;
2984
+
2985
+ const StreamingHandler = createHandler<StreamingFn>(
2986
+ async (req, def, handle, ctx) => {
2987
+ const data = await req.json();
2988
+ const stream = handle(data, ctx);
2989
+
2990
+ return new Response(
2991
+ new ReadableStream({
2992
+ async start(controller) {
2993
+ for await (const chunk of stream) {
2994
+ controller.enqueue(new TextEncoder().encode(chunk));
2995
+ }
2996
+ controller.close();
2997
+ },
2998
+ })
2999
+ );
3000
+ }
3001
+ );
3002
+
3003
+ export const myPlugin = createPlugin.define({
3004
+ handlers: {
3005
+ streaming: StreamingHandler,
3006
+ },
3007
+ // ...
3008
+ });
3009
+ \`\`\`
3010
+
3011
+ Usage:
3012
+ \`\`\`typescript
3013
+ router.route("stream").streaming({
3014
+ handle: async function* (data, ctx) {
3015
+ yield "Starting...";
3016
+ yield "Processing...";
3017
+ yield "Done!";
3018
+ },
3019
+ });
3020
+ \`\`\`
3021
+
3022
+ ## After Making Changes
3023
+ Always run \`donkeylabs generate\` to update types after:
3024
+ - Adding/modifying plugins
3025
+ - Adding routes in src/routes/
3026
+ - Adding custom handlers
3027
+ - Adding middleware
3028
+
3029
+ ## Common Patterns
3030
+
3031
+ ### Creating a Plugin with Database
3032
+ \`\`\`typescript
3033
+ import { createPlugin } from "@donkeylabs/server";
3034
+ import type { DB as MySchema } from "./schema";
3035
+
3036
+ export const myPlugin = createPlugin
3037
+ .withSchema<MySchema>()
3038
+ .define({
3039
+ name: "my-plugin",
3040
+ version: "1.0.0",
3041
+ service: async (ctx) => ({
3042
+ getData: async () => {
3043
+ return ctx.db.selectFrom("my_table").selectAll().execute();
3044
+ }
3045
+ }),
3046
+ });
3047
+ \`\`\`
3048
+
3049
+ ### Creating a Typed Route
3050
+ \`\`\`typescript
3051
+ router.route("getUser").typed({
3052
+ input: z.object({ id: z.string() }),
3053
+ output: z.object({ user: userSchema }),
3054
+ handle: async (input, ctx) => {
3055
+ const user = await ctx.plugins.users.getById(input.id);
3056
+ if (!user) throw ctx.errors.NotFound("User not found");
3057
+ return { user };
3058
+ },
3059
+ });
3060
+ \`\`\`
3061
+
3062
+ ### Plugin Dependencies
3063
+ \`\`\`typescript
3064
+ export const statsPlugin = createPlugin.define({
3065
+ name: "stats",
3066
+ dependencies: ["auth", "counter"] as const,
3067
+ service: async (ctx) => ({
3068
+ getStats: async () => {
3069
+ // Access dependencies via ctx.deps
3070
+ const user = ctx.deps.auth.getCurrentUser();
3071
+ const count = ctx.deps.counter.get("visits");
3072
+ return { user, count };
3073
+ }
3074
+ }),
3075
+ });
3076
+ \`\`\`
3077
+
3078
+ ## Error Handling
3079
+ Use \`ctx.errors\` factories:
3080
+ - \`ctx.errors.BadRequest(message)\`
3081
+ - \`ctx.errors.Unauthorized(message)\`
3082
+ - \`ctx.errors.Forbidden(message)\`
3083
+ - \`ctx.errors.NotFound(message)\`
3084
+ - \`ctx.errors.Conflict(message)\`
3085
+
3086
+ ## After Making Changes
3087
+ Always run \`donkeylabs generate\` to update types after:
3088
+ - Adding/modifying plugins
3089
+ - Adding custom handlers
3090
+ - Adding middleware
3091
+ `;
3092
+
3093
+ const server = new Server(
3094
+ {
3095
+ name: "donkeylabs-server",
3096
+ version: "0.1.0",
3097
+ },
3098
+ {
3099
+ capabilities: {
3100
+ tools: {},
3101
+ resources: {},
3102
+ },
3103
+ }
3104
+ );
3105
+
3106
+ // Resource handlers
3107
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
3108
+ resources: [
3109
+ {
3110
+ uri: "donkeylabs://context",
3111
+ name: "Project Development Guide",
3112
+ description: "Conventions, patterns, and best practices for @donkeylabs/server development",
3113
+ mimeType: "text/markdown",
3114
+ },
3115
+ ],
3116
+ }));
3117
+
3118
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
3119
+ const { uri } = request.params;
3120
+
3121
+ if (uri === "donkeylabs://context") {
3122
+ return {
3123
+ contents: [
3124
+ {
3125
+ uri,
3126
+ mimeType: "text/markdown",
3127
+ text: PROJECT_CONTEXT,
3128
+ },
3129
+ ],
3130
+ };
3131
+ }
3132
+
3133
+ throw new Error(`Unknown resource: ${uri}`);
3134
+ });
3135
+
3136
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
3137
+ tools,
3138
+ }));
3139
+
3140
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3141
+ const { name, arguments: args } = request.params;
3142
+
3143
+ try {
3144
+ let result: string;
3145
+
3146
+ switch (name) {
3147
+ case "init_project":
3148
+ result = await initProject(args as any);
3149
+ break;
3150
+ case "create_plugin":
3151
+ result = await createPlugin(args as any);
3152
+ break;
3153
+ case "add_route":
3154
+ result = await addRoute(args as any);
3155
+ break;
3156
+ case "add_migration":
3157
+ result = await addMigration(args as any);
3158
+ break;
3159
+ case "generate_types":
3160
+ result = await generateTypes();
3161
+ break;
3162
+ case "list_plugins":
3163
+ result = await listPlugins();
3164
+ break;
3165
+ case "get_project_info":
3166
+ result = await getProjectInfo();
3167
+ break;
3168
+ case "add_service_method":
3169
+ result = await addServiceMethod(args as any);
3170
+ break;
3171
+ case "create_router":
3172
+ result = await createRouter(args as any);
3173
+ break;
3174
+ case "validate_project":
3175
+ result = await validateProject();
3176
+ break;
3177
+ case "register_plugin":
3178
+ result = await registerPlugin(args as any);
3179
+ break;
3180
+ case "run_migrations":
3181
+ result = await runMigrations(args as any);
3182
+ break;
3183
+ case "setup_database":
3184
+ result = await setupDatabase(args as any);
3185
+ break;
3186
+ case "list_routes":
3187
+ result = await listRoutes(args as any);
3188
+ break;
3189
+ case "add_route_file":
3190
+ result = await addRouteFile(args as any);
3191
+ break;
3192
+ case "create_model":
3193
+ result = await createModel(args as any);
3194
+ break;
3195
+ case "add_unit_test":
3196
+ result = await addUnitTest(args as any);
3197
+ break;
3198
+ case "add_integration_test":
3199
+ result = await addIntegrationTest(args as any);
3200
+ break;
3201
+ case "generate_client":
3202
+ result = await generateClientTool(args as any);
3203
+ break;
3204
+ case "add_middleware":
3205
+ result = await addMiddleware(args as any);
3206
+ break;
3207
+ case "add_custom_handler":
3208
+ result = await addCustomHandler(args as any);
3209
+ break;
3210
+ case "create_custom_error":
3211
+ result = await createCustomError(args as any);
3212
+ break;
3213
+ case "validate_code":
3214
+ result = await validateCode(args as any);
3215
+ break;
3216
+ default:
3217
+ throw new Error(`Unknown tool: ${name}`);
3218
+ }
3219
+
3220
+ return {
3221
+ content: [{ type: "text", text: result }],
3222
+ };
3223
+ } catch (error: any) {
3224
+ return {
3225
+ content: [{ type: "text", text: `Error: ${error.message}` }],
3226
+ isError: true,
3227
+ };
3228
+ }
3229
+ });
3230
+
3231
+ // Start the server
3232
+ async function main() {
3233
+ const transport = new StdioServerTransport();
3234
+ await server.connect(transport);
3235
+ console.error("@donkeylabs/server MCP server running on stdio");
3236
+ }
3237
+
3238
+ main().catch(console.error);