@donkeylabs/mcp 0.4.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/package.json +53 -0
  4. package/src/server.ts +2524 -0
package/src/server.ts ADDED
@@ -0,0 +1,2524 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * @donkeylabs/mcp - MCP Server for AI-assisted development
4
+ *
5
+ * Provides tools and resources for AI assistants to scaffold and manage @donkeylabs/server projects.
6
+ * Helps agents follow best practices and avoid creating unmaintainable code.
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
+ } from "@modelcontextprotocol/sdk/types.js";
17
+ import { join, dirname, basename, relative } from "path";
18
+ import { existsSync, mkdirSync, readdirSync, statSync, readFileSync } from "fs";
19
+
20
+ // =============================================================================
21
+ // PROJECT DETECTION & HELPERS
22
+ // =============================================================================
23
+
24
+ function findProjectRoot(startDir: string = process.cwd()): string | null {
25
+ let dir = startDir;
26
+ while (dir !== "/") {
27
+ if (existsSync(join(dir, "donkeylabs.config.ts"))) {
28
+ return dir;
29
+ }
30
+ const pkgPath = join(dir, "package.json");
31
+ if (existsSync(pkgPath)) {
32
+ try {
33
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
34
+ if (
35
+ pkg.dependencies?.["@donkeylabs/server"] ||
36
+ pkg.devDependencies?.["@donkeylabs/server"]
37
+ ) {
38
+ return dir;
39
+ }
40
+ } catch {}
41
+ }
42
+ dir = dirname(dir);
43
+ }
44
+ return process.cwd();
45
+ }
46
+
47
+ const projectRoot = findProjectRoot() || process.cwd();
48
+
49
+ // =============================================================================
50
+ // PROJECT CONFIG DETECTION
51
+ // =============================================================================
52
+
53
+ interface ProjectConfig {
54
+ adapter?: string;
55
+ pluginsDir: string;
56
+ routesDir: string;
57
+ clientOutput?: string;
58
+ }
59
+
60
+ function detectProjectConfig(): ProjectConfig {
61
+ const configPath = join(projectRoot, "donkeylabs.config.ts");
62
+
63
+ // Default paths for standalone server
64
+ let config: ProjectConfig = {
65
+ pluginsDir: "src/plugins",
66
+ routesDir: "src/routes",
67
+ };
68
+
69
+ if (existsSync(configPath)) {
70
+ try {
71
+ const content = readFileSync(configPath, "utf-8");
72
+
73
+ // Check for SvelteKit adapter
74
+ if (content.includes("adapter-sveltekit") || content.includes("@donkeylabs/adapter-sveltekit")) {
75
+ config.adapter = "sveltekit";
76
+ config.pluginsDir = "src/server/plugins";
77
+ config.routesDir = "src/server/routes";
78
+ }
79
+
80
+ // Check for custom plugins path
81
+ const pluginsMatch = content.match(/plugins:\s*\["([^"]+)"/);
82
+ if (pluginsMatch) {
83
+ const pluginPath = pluginsMatch[1];
84
+ if (pluginPath.includes("src/server/plugins")) {
85
+ config.pluginsDir = "src/server/plugins";
86
+ } else if (pluginPath.includes("src/plugins")) {
87
+ config.pluginsDir = "src/plugins";
88
+ }
89
+ }
90
+
91
+ // Check for custom routes path
92
+ const routesMatch = content.match(/routes:\s*"([^"]+)"/);
93
+ if (routesMatch) {
94
+ const routePath = routesMatch[1];
95
+ if (routePath.includes("src/server/routes")) {
96
+ config.routesDir = "src/server/routes";
97
+ }
98
+ }
99
+
100
+ // Check for client output
101
+ const clientMatch = content.match(/client:\s*\{[^}]*output:\s*"([^"]+)"/);
102
+ if (clientMatch) {
103
+ config.clientOutput = clientMatch[1];
104
+ }
105
+ } catch (e) {
106
+ // Use defaults on error
107
+ }
108
+ }
109
+
110
+ return config;
111
+ }
112
+
113
+ const projectConfig = detectProjectConfig();
114
+
115
+ function toPascalCase(str: string): string {
116
+ return str
117
+ .replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => word.toUpperCase())
118
+ .replace(/[\s-_]+/g, "");
119
+ }
120
+
121
+ function toKebabCase(str: string): string {
122
+ return str
123
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
124
+ .replace(/[\s_]+/g, "-")
125
+ .toLowerCase();
126
+ }
127
+
128
+ function isCamelCase(str: string): boolean {
129
+ return /^[a-z][a-zA-Z0-9]*$/.test(str);
130
+ }
131
+
132
+ // =============================================================================
133
+ // VALIDATION HELPERS
134
+ // =============================================================================
135
+
136
+ interface ValidationResult {
137
+ valid: boolean;
138
+ error?: string;
139
+ suggestion?: string;
140
+ }
141
+
142
+ function validateProjectExists(): ValidationResult {
143
+ const configPath = join(projectRoot, "donkeylabs.config.ts");
144
+ const pkgPath = join(projectRoot, "package.json");
145
+
146
+ if (!existsSync(configPath) && !existsSync(pkgPath)) {
147
+ return {
148
+ valid: false,
149
+ error: "No @donkeylabs/server project found",
150
+ suggestion: "Run 'bunx @donkeylabs/cli init' to create a new project, or navigate to an existing project directory.",
151
+ };
152
+ }
153
+ return { valid: true };
154
+ }
155
+
156
+ function validatePluginName(name: string): ValidationResult {
157
+ if (!isCamelCase(name)) {
158
+ return {
159
+ valid: false,
160
+ error: `Plugin name "${name}" is not in camelCase`,
161
+ suggestion: `Use camelCase naming (e.g., "${name.charAt(0).toLowerCase()}${name.slice(1).replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase())}")`,
162
+ };
163
+ }
164
+ return { valid: true };
165
+ }
166
+
167
+ function validatePluginExists(name: string): ValidationResult {
168
+ const pluginDir = join(projectRoot, projectConfig.pluginsDir, name);
169
+ if (!existsSync(pluginDir)) {
170
+ const availablePlugins = listAvailablePlugins();
171
+ return {
172
+ valid: false,
173
+ error: `Plugin "${name}" not found at ${projectConfig.pluginsDir}/${name}/`,
174
+ suggestion: availablePlugins.length > 0
175
+ ? `Available plugins: ${availablePlugins.join(", ")}`
176
+ : "Create a plugin first using the create_plugin tool.",
177
+ };
178
+ }
179
+ return { valid: true };
180
+ }
181
+
182
+ function validateRouterExists(routerFile: string): ValidationResult {
183
+ const fullPath = join(projectRoot, routerFile);
184
+ if (!existsSync(fullPath)) {
185
+ const availableRouters = listAvailableRouters();
186
+ return {
187
+ valid: false,
188
+ error: `Router file not found: ${routerFile}`,
189
+ suggestion: availableRouters.length > 0
190
+ ? `Available routers:\n${availableRouters.map(r => ` - ${r}`).join("\n")}\n\nOr use create_router to create a new one.`
191
+ : "Create a router first using the create_router tool.",
192
+ };
193
+ }
194
+ return { valid: true };
195
+ }
196
+
197
+ function listAvailablePlugins(): string[] {
198
+ const pluginsDir = join(projectRoot, projectConfig.pluginsDir);
199
+ if (!existsSync(pluginsDir)) return [];
200
+
201
+ return readdirSync(pluginsDir)
202
+ .filter((d) => statSync(join(pluginsDir, d)).isDirectory())
203
+ .filter((d) => existsSync(join(pluginsDir, d, "index.ts")));
204
+ }
205
+
206
+ function listAvailableRouters(): string[] {
207
+ const routesDir = join(projectRoot, projectConfig.routesDir);
208
+ if (!existsSync(routesDir)) {
209
+ return [];
210
+ }
211
+ return findRouterFiles(routesDir, projectConfig.routesDir);
212
+ }
213
+
214
+ function findRouterFiles(dir: string, prefix: string): string[] {
215
+ const files: string[] = [];
216
+
217
+ function scan(currentDir: string, currentPrefix: string) {
218
+ for (const entry of readdirSync(currentDir)) {
219
+ const fullPath = join(currentDir, entry);
220
+ const relativePath = `${currentPrefix}/${entry}`;
221
+
222
+ if (statSync(fullPath).isDirectory()) {
223
+ const indexPath = join(fullPath, "index.ts");
224
+ if (existsSync(indexPath)) {
225
+ files.push(`${relativePath}/index.ts`);
226
+ }
227
+ scan(fullPath, relativePath);
228
+ } else if (entry.endsWith(".ts") && !entry.includes(".test.") && !entry.includes(".spec.")) {
229
+ files.push(relativePath);
230
+ }
231
+ }
232
+ }
233
+
234
+ scan(dir, prefix);
235
+ return files;
236
+ }
237
+
238
+ function formatError(error: string, context?: string, suggestion?: string, relatedTool?: string): string {
239
+ let message = `## Error: ${error}\n`;
240
+
241
+ if (context) {
242
+ message += `\n### Context\n${context}\n`;
243
+ }
244
+
245
+ if (suggestion) {
246
+ message += `\n### How to Fix\n${suggestion}\n`;
247
+ }
248
+
249
+ if (relatedTool) {
250
+ message += `\n### Related Tool\nConsider using: \`${relatedTool}\`\n`;
251
+ }
252
+
253
+ return message;
254
+ }
255
+
256
+ // =============================================================================
257
+ // RESOURCE DEFINITIONS
258
+ // =============================================================================
259
+
260
+ // Resolve docs directory relative to this file's location
261
+ // This handles running from different working directories
262
+ function findDocsDir(): string {
263
+ const possiblePaths = [
264
+ // When running from monorepo
265
+ join(import.meta.dir, "..", "..", "..", "server", "docs"),
266
+ // When installed as package
267
+ join(import.meta.dir, "..", "..", "node_modules", "@donkeylabs", "server", "docs"),
268
+ // Fallback to trying to find it from cwd
269
+ join(process.cwd(), "node_modules", "@donkeylabs", "server", "docs"),
270
+ ];
271
+
272
+ for (const p of possiblePaths) {
273
+ if (existsSync(p)) {
274
+ return p;
275
+ }
276
+ }
277
+
278
+ // Return first path even if not found (will show appropriate error later)
279
+ return possiblePaths[0];
280
+ }
281
+
282
+ const DOCS_DIR = findDocsDir();
283
+
284
+ const RESOURCES = [
285
+ {
286
+ uri: "donkeylabs://docs/project-structure",
287
+ name: "Project Structure",
288
+ description: "Canonical directory layout, naming conventions, and organizational patterns",
289
+ mimeType: "text/markdown",
290
+ docFile: "project-structure.md",
291
+ },
292
+ {
293
+ uri: "donkeylabs://docs/plugins",
294
+ name: "Plugins Guide",
295
+ description: "Creating plugins with services, database schemas, middleware, and dependencies",
296
+ mimeType: "text/markdown",
297
+ docFile: "plugins.md",
298
+ },
299
+ {
300
+ uri: "donkeylabs://docs/router",
301
+ name: "Router & Routes",
302
+ description: "Route definitions, handlers (typed, raw, class-based), middleware chaining",
303
+ mimeType: "text/markdown",
304
+ docFile: "router.md",
305
+ },
306
+ {
307
+ uri: "donkeylabs://docs/handlers",
308
+ name: "Custom Handlers",
309
+ description: "Creating custom request handlers for specialized processing",
310
+ mimeType: "text/markdown",
311
+ docFile: "handlers.md",
312
+ },
313
+ {
314
+ uri: "donkeylabs://docs/middleware",
315
+ name: "Middleware",
316
+ description: "Creating and using middleware for cross-cutting concerns",
317
+ mimeType: "text/markdown",
318
+ docFile: "middleware.md",
319
+ },
320
+ {
321
+ uri: "donkeylabs://docs/core-services",
322
+ name: "Core Services Overview",
323
+ description: "Overview of all built-in core services",
324
+ mimeType: "text/markdown",
325
+ docFile: "core-services.md",
326
+ },
327
+ {
328
+ uri: "donkeylabs://docs/logger",
329
+ name: "Logger Service",
330
+ description: "Structured logging with child loggers and context",
331
+ mimeType: "text/markdown",
332
+ docFile: "logger.md",
333
+ },
334
+ {
335
+ uri: "donkeylabs://docs/cache",
336
+ name: "Cache Service",
337
+ description: "In-memory caching with TTL support",
338
+ mimeType: "text/markdown",
339
+ docFile: "cache.md",
340
+ },
341
+ {
342
+ uri: "donkeylabs://docs/events",
343
+ name: "Events Service",
344
+ description: "Pub/sub event system with pattern matching",
345
+ mimeType: "text/markdown",
346
+ docFile: "events.md",
347
+ },
348
+ {
349
+ uri: "donkeylabs://docs/cron",
350
+ name: "Cron Service",
351
+ description: "Scheduled task execution with cron expressions",
352
+ mimeType: "text/markdown",
353
+ docFile: "cron.md",
354
+ },
355
+ {
356
+ uri: "donkeylabs://docs/jobs",
357
+ name: "Jobs Service",
358
+ description: "Background job queue with retries and scheduling",
359
+ mimeType: "text/markdown",
360
+ docFile: "jobs.md",
361
+ },
362
+ {
363
+ uri: "donkeylabs://docs/sse",
364
+ name: "SSE Service",
365
+ description: "Server-Sent Events for real-time updates",
366
+ mimeType: "text/markdown",
367
+ docFile: "sse.md",
368
+ },
369
+ {
370
+ uri: "donkeylabs://docs/rate-limiter",
371
+ name: "Rate Limiter",
372
+ description: "Per-key request rate limiting",
373
+ mimeType: "text/markdown",
374
+ docFile: "rate-limiter.md",
375
+ },
376
+ {
377
+ uri: "donkeylabs://docs/errors",
378
+ name: "Error Handling",
379
+ description: "HTTP errors and custom error types",
380
+ mimeType: "text/markdown",
381
+ docFile: "errors.md",
382
+ },
383
+ {
384
+ uri: "donkeylabs://docs/cli",
385
+ name: "CLI Commands",
386
+ description: "Command-line interface for code generation and project management",
387
+ mimeType: "text/markdown",
388
+ docFile: "cli.md",
389
+ },
390
+ {
391
+ uri: "donkeylabs://docs/api-client",
392
+ name: "API Client Generation",
393
+ description: "Type-safe client generation from route definitions",
394
+ mimeType: "text/markdown",
395
+ docFile: "api-client.md",
396
+ },
397
+ {
398
+ uri: "donkeylabs://project/current",
399
+ name: "Current Project Analysis",
400
+ description: "Dynamic analysis of the current project structure",
401
+ mimeType: "text/markdown",
402
+ dynamic: true,
403
+ },
404
+ ];
405
+
406
+ async function readResource(uri: string): Promise<string> {
407
+ const resource = RESOURCES.find((r) => r.uri === uri);
408
+ if (!resource) {
409
+ return `Resource not found: ${uri}`;
410
+ }
411
+
412
+ // Dynamic resources
413
+ if (resource.dynamic) {
414
+ if (uri === "donkeylabs://project/current") {
415
+ return await generateProjectAnalysis();
416
+ }
417
+ }
418
+
419
+ // Static doc files
420
+ if (resource.docFile) {
421
+ const docPath = join(DOCS_DIR, resource.docFile);
422
+ if (existsSync(docPath)) {
423
+ return readFileSync(docPath, "utf-8");
424
+ }
425
+ return `Documentation file not found: ${resource.docFile}`;
426
+ }
427
+
428
+ return `Resource content not available: ${uri}`;
429
+ }
430
+
431
+ async function generateProjectAnalysis(): Promise<string> {
432
+ const validation = validateProjectExists();
433
+ if (!validation.valid) {
434
+ return formatError(validation.error!, undefined, validation.suggestion);
435
+ }
436
+
437
+ let analysis = `# Project Analysis: ${basename(projectRoot)}\n\n`;
438
+
439
+ // Config
440
+ const configPath = join(projectRoot, "donkeylabs.config.ts");
441
+ analysis += `## Configuration\n`;
442
+ if (existsSync(configPath)) {
443
+ analysis += `- Config file: donkeylabs.config.ts\n`;
444
+ if (projectConfig.adapter) {
445
+ analysis += `- Adapter: ${projectConfig.adapter}\n`;
446
+ }
447
+ analysis += `- Plugins directory: ${projectConfig.pluginsDir}/\n`;
448
+ analysis += `- Routes directory: ${projectConfig.routesDir}/\n`;
449
+ if (projectConfig.clientOutput) {
450
+ analysis += `- Client output: ${projectConfig.clientOutput}\n`;
451
+ }
452
+ } else {
453
+ analysis += `- No donkeylabs.config.ts found\n`;
454
+ }
455
+
456
+ // Plugins
457
+ const plugins = listAvailablePlugins();
458
+ analysis += `\n## Plugins (${plugins.length})\n`;
459
+ if (plugins.length === 0) {
460
+ analysis += `No plugins found. Create one with \`create_plugin\` tool.\n`;
461
+ } else {
462
+ for (const plugin of plugins) {
463
+ const pluginPath = join(projectRoot, projectConfig.pluginsDir, plugin, "index.ts");
464
+ const content = readFileSync(pluginPath, "utf-8");
465
+
466
+ const hasMigrations = existsSync(join(projectRoot, projectConfig.pluginsDir, plugin, "migrations"));
467
+ const hasSchema = existsSync(join(projectRoot, projectConfig.pluginsDir, plugin, "schema.ts"));
468
+ const depsMatch = content.match(/dependencies:\s*\[([^\]]*)\]/);
469
+
470
+ analysis += `\n### ${plugin}\n`;
471
+ analysis += `- Path: ${projectConfig.pluginsDir}/${plugin}/\n`;
472
+ analysis += `- Database: ${hasSchema || hasMigrations ? "yes" : "no"}\n`;
473
+ if (depsMatch && depsMatch[1].trim()) {
474
+ analysis += `- Dependencies: ${depsMatch[1].trim()}\n`;
475
+ }
476
+
477
+ // Extract service methods
478
+ const serviceMatch = content.match(/service:\s*async\s*\([^)]*\)\s*=>\s*\({([^}]+)\}/s);
479
+ if (serviceMatch) {
480
+ const methods = [...serviceMatch[1].matchAll(/(\w+):\s*(?:async\s*)?\(/g)].map(m => m[1]);
481
+ if (methods.length > 0) {
482
+ analysis += `- Methods: ${methods.join(", ")}\n`;
483
+ }
484
+ }
485
+ }
486
+ }
487
+
488
+ // Routes
489
+ const routers = listAvailableRouters();
490
+ analysis += `\n## Routes (${routers.length} files)\n`;
491
+ if (routers.length === 0) {
492
+ analysis += `No route files found. Create one with \`create_router\` tool.\n`;
493
+ } else {
494
+ for (const router of routers.slice(0, 10)) {
495
+ analysis += `- ${router}\n`;
496
+ }
497
+ if (routers.length > 10) {
498
+ analysis += `- ... and ${routers.length - 10} more\n`;
499
+ }
500
+ }
501
+
502
+ // Generated types
503
+ const genDir = join(projectRoot, ".@donkeylabs/server");
504
+ analysis += `\n## Generated Types\n`;
505
+ if (existsSync(genDir)) {
506
+ analysis += `- Output: .@donkeylabs/server/\n`;
507
+ analysis += `- Run \`donkeylabs generate\` to regenerate after changes\n`;
508
+ } else {
509
+ analysis += `- Not generated yet. Run \`donkeylabs generate\` after adding plugins/routes.\n`;
510
+ }
511
+
512
+ return analysis;
513
+ }
514
+
515
+ // =============================================================================
516
+ // TOOL DEFINITIONS
517
+ // =============================================================================
518
+
519
+ const tools = [
520
+ {
521
+ name: "get_project_info",
522
+ description: "Get current project structure, plugins, and routes. Run this first to understand the project.",
523
+ inputSchema: {
524
+ type: "object" as const,
525
+ properties: {},
526
+ },
527
+ },
528
+ {
529
+ name: "get_architecture_guidance",
530
+ description: "Get step-by-step guidance for implementing a feature. Describes which tools to use in what order.",
531
+ inputSchema: {
532
+ type: "object" as const,
533
+ properties: {
534
+ task: {
535
+ type: "string",
536
+ description: "What you want to accomplish (e.g., 'add user authentication', 'create CRUD for products')",
537
+ },
538
+ },
539
+ required: ["task"],
540
+ },
541
+ },
542
+ {
543
+ name: "create_plugin",
544
+ description: `Create a new plugin with correct directory structure.
545
+
546
+ **Plugins can implement:**
547
+ - **service**: Business logic methods (ctx.plugins.name.method())
548
+ - **init hook**: Cron jobs, event listeners, job registration
549
+ - **customErrors**: Typed error definitions
550
+ - **events**: Typed event schemas for SSE/pub-sub
551
+ - **middleware**: Request middleware (auth, rate limiting, etc.)
552
+ - **handlers**: Custom request handlers (beyond typed/raw)
553
+
554
+ **Plugin modifiers:**
555
+ - withSchema<T>(): Add typed database access
556
+ - withConfig<T>(): Make plugin configurable at registration`,
557
+ inputSchema: {
558
+ type: "object" as const,
559
+ properties: {
560
+ name: {
561
+ type: "string",
562
+ description: "Plugin name in camelCase (e.g., 'auth', 'users', 'orders')",
563
+ },
564
+ hasSchema: {
565
+ type: "boolean",
566
+ description: "Whether the plugin needs database schema (creates migrations folder and uses withSchema<>())",
567
+ default: false,
568
+ },
569
+ hasConfig: {
570
+ type: "boolean",
571
+ description: "Whether the plugin accepts configuration at registration (uses withConfig<>())",
572
+ default: false,
573
+ },
574
+ configFields: {
575
+ type: "string",
576
+ description: "If hasConfig=true, TypeScript interface fields for config (e.g., 'apiKey: string; sandbox?: boolean')",
577
+ },
578
+ dependencies: {
579
+ type: "array",
580
+ items: { type: "string" },
581
+ description: "Names of plugins this plugin depends on",
582
+ default: [],
583
+ },
584
+ },
585
+ required: ["name"],
586
+ },
587
+ },
588
+ {
589
+ name: "add_service_method",
590
+ description: "Add a method to a plugin's service. Service methods contain business logic.",
591
+ inputSchema: {
592
+ type: "object" as const,
593
+ properties: {
594
+ pluginName: {
595
+ type: "string",
596
+ description: "Name of the plugin",
597
+ },
598
+ methodName: {
599
+ type: "string",
600
+ description: "Name of the method (camelCase)",
601
+ },
602
+ params: {
603
+ type: "string",
604
+ description: "Method parameters (e.g., 'userId: string, data: { name: string }')",
605
+ },
606
+ returnType: {
607
+ type: "string",
608
+ description: "Return type (e.g., 'Promise<User>', '{ id: string }')",
609
+ },
610
+ implementation: {
611
+ type: "string",
612
+ description: "Method implementation code",
613
+ },
614
+ },
615
+ required: ["pluginName", "methodName", "implementation"],
616
+ },
617
+ },
618
+ {
619
+ name: "add_migration",
620
+ description: "Create a numbered SQL migration file for a plugin's database schema.",
621
+ inputSchema: {
622
+ type: "object" as const,
623
+ properties: {
624
+ pluginName: {
625
+ type: "string",
626
+ description: "Name of the plugin",
627
+ },
628
+ migrationName: {
629
+ type: "string",
630
+ description: "Descriptive name (e.g., 'create_users', 'add_email_column')",
631
+ },
632
+ upSql: {
633
+ type: "string",
634
+ description: "SQL for the up migration (CREATE TABLE, ALTER TABLE, etc.)",
635
+ },
636
+ downSql: {
637
+ type: "string",
638
+ description: "SQL for the down migration (DROP TABLE, etc.)",
639
+ },
640
+ },
641
+ required: ["pluginName", "migrationName", "upSql"],
642
+ },
643
+ },
644
+ {
645
+ name: "create_router",
646
+ description: "Create a new router file with proper structure. Routers group related routes.",
647
+ inputSchema: {
648
+ type: "object" as const,
649
+ properties: {
650
+ routerPath: {
651
+ type: "string",
652
+ description: "Path for new router (e.g., 'src/routes/users/index.ts')",
653
+ },
654
+ routerName: {
655
+ type: "string",
656
+ description: "Export name for router (e.g., 'usersRouter')",
657
+ },
658
+ prefix: {
659
+ type: "string",
660
+ description: "Route prefix - routes will be named 'prefix.routeName' (e.g., 'users')",
661
+ },
662
+ },
663
+ required: ["routerPath", "routerName", "prefix"],
664
+ },
665
+ },
666
+ {
667
+ name: "add_route",
668
+ description: "Add a new route to an existing router. Creates a class-based handler by default.",
669
+ inputSchema: {
670
+ type: "object" as const,
671
+ properties: {
672
+ routerFile: {
673
+ type: "string",
674
+ description: "Path to the router file (relative to project root)",
675
+ },
676
+ routeName: {
677
+ type: "string",
678
+ description: "Route name (e.g., 'list', 'get', 'create')",
679
+ },
680
+ inputSchema: {
681
+ type: "string",
682
+ description: "Zod schema for input validation (e.g., 'z.object({ id: z.string() })')",
683
+ },
684
+ outputType: {
685
+ type: "string",
686
+ description: "Zod schema for output validation (optional)",
687
+ },
688
+ handler: {
689
+ type: "string",
690
+ description: "Handler implementation code (the body of the handle method)",
691
+ },
692
+ useClassHandler: {
693
+ type: "boolean",
694
+ description: "Generate a class-based handler in handlers/ directory (recommended)",
695
+ default: true,
696
+ },
697
+ },
698
+ required: ["routerFile", "routeName", "handler"],
699
+ },
700
+ },
701
+ {
702
+ name: "add_handler_to_router",
703
+ description: "Register an existing handler class to a router (when handler already exists).",
704
+ inputSchema: {
705
+ type: "object" as const,
706
+ properties: {
707
+ routerFile: {
708
+ type: "string",
709
+ description: "Path to router file",
710
+ },
711
+ handlerName: {
712
+ type: "string",
713
+ description: "Handler class name to import",
714
+ },
715
+ handlerPath: {
716
+ type: "string",
717
+ description: "Relative import path for handler (e.g., './handlers/create-user')",
718
+ },
719
+ routeName: {
720
+ type: "string",
721
+ description: "Route name",
722
+ },
723
+ inputSchema: {
724
+ type: "string",
725
+ description: "Zod input schema",
726
+ },
727
+ outputSchema: {
728
+ type: "string",
729
+ description: "Zod output schema (optional)",
730
+ },
731
+ },
732
+ required: ["routerFile", "handlerName", "handlerPath", "routeName"],
733
+ },
734
+ },
735
+ {
736
+ name: "extend_plugin",
737
+ description: `Add custom errors, events, middleware, or handlers to an existing plugin.
738
+
739
+ **Extension types:**
740
+ - **error**: Custom error type (e.g., UserNotFound with status 404)
741
+ - **event**: Typed event for SSE/pub-sub
742
+ - **middleware**: Request middleware (auth, rate limiting, etc.)
743
+ - **handler**: Custom request handler (beyond typed/raw)`,
744
+ inputSchema: {
745
+ type: "object" as const,
746
+ properties: {
747
+ pluginName: {
748
+ type: "string",
749
+ description: "Name of the plugin",
750
+ },
751
+ extensionType: {
752
+ type: "string",
753
+ enum: ["error", "event", "middleware", "handler"],
754
+ description: "Type of extension to add",
755
+ },
756
+ name: {
757
+ type: "string",
758
+ description: "Name of the extension (e.g., 'UserNotFound' for error, 'user.created' for event, 'xml' for handler)",
759
+ },
760
+ params: {
761
+ type: "object",
762
+ description: "Extension-specific parameters",
763
+ properties: {
764
+ code: { type: "string", description: "Error code (for errors)" },
765
+ status: { type: "number", description: "HTTP status code (for errors)" },
766
+ message: { type: "string", description: "Default message (for errors)" },
767
+ schema: { type: "string", description: "Zod schema (for events)" },
768
+ implementation: { type: "string", description: "Middleware or handler implementation code" },
769
+ handlerSignature: { type: "string", description: "TypeScript type for handler function (for handlers)" },
770
+ },
771
+ },
772
+ },
773
+ required: ["pluginName", "extensionType", "name"],
774
+ },
775
+ },
776
+ {
777
+ name: "add_cron",
778
+ description: "Schedule a new cron job in a plugin's init hook.",
779
+ inputSchema: {
780
+ type: "object" as const,
781
+ properties: {
782
+ pluginName: {
783
+ type: "string",
784
+ description: "Plugin to add the cron job to",
785
+ },
786
+ name: {
787
+ type: "string",
788
+ description: "Cron job name (for logging/identification)",
789
+ },
790
+ schedule: {
791
+ type: "string",
792
+ description: "Cron schedule expression (e.g., '0 * * * *' for hourly, '0 0 * * *' for daily)",
793
+ },
794
+ implementation: {
795
+ type: "string",
796
+ description: "Task implementation code",
797
+ },
798
+ },
799
+ required: ["pluginName", "name", "schedule", "implementation"],
800
+ },
801
+ },
802
+ {
803
+ name: "add_event",
804
+ description: "Register a new event type with its schema in a plugin.",
805
+ inputSchema: {
806
+ type: "object" as const,
807
+ properties: {
808
+ pluginName: {
809
+ type: "string",
810
+ description: "Plugin to register the event in",
811
+ },
812
+ name: {
813
+ type: "string",
814
+ description: "Event name (e.g., 'user.created', 'order.completed')",
815
+ },
816
+ schema: {
817
+ type: "string",
818
+ description: "Zod schema for event data (e.g., 'z.object({ userId: z.string() })')",
819
+ },
820
+ },
821
+ required: ["pluginName", "name", "schema"],
822
+ },
823
+ },
824
+ {
825
+ name: "add_async_job",
826
+ description: "Register a new background job handler in a plugin.",
827
+ inputSchema: {
828
+ type: "object" as const,
829
+ properties: {
830
+ pluginName: {
831
+ type: "string",
832
+ description: "Plugin to register the job in",
833
+ },
834
+ name: {
835
+ type: "string",
836
+ description: "Job name (e.g., 'send-email', 'process-payment')",
837
+ },
838
+ implementation: {
839
+ type: "string",
840
+ description: "Job handler implementation code",
841
+ },
842
+ },
843
+ required: ["pluginName", "name", "implementation"],
844
+ },
845
+ },
846
+ {
847
+ name: "add_sse_route",
848
+ description: "Add a Server-Sent Events (SSE) route for real-time updates.",
849
+ inputSchema: {
850
+ type: "object" as const,
851
+ properties: {
852
+ routerFile: {
853
+ type: "string",
854
+ description: "Path to router file",
855
+ },
856
+ routeName: {
857
+ type: "string",
858
+ description: "Route name for SSE endpoint",
859
+ },
860
+ channel: {
861
+ type: "string",
862
+ description: "SSE channel name to subscribe to",
863
+ },
864
+ },
865
+ required: ["routerFile", "routeName", "channel"],
866
+ },
867
+ },
868
+ {
869
+ name: "list_plugins",
870
+ description: "List all plugins with their service methods and dependencies.",
871
+ inputSchema: {
872
+ type: "object" as const,
873
+ properties: {},
874
+ },
875
+ },
876
+ {
877
+ name: "generate_types",
878
+ description: "Run type generation to update registry and context types after making changes.",
879
+ inputSchema: {
880
+ type: "object" as const,
881
+ properties: {
882
+ target: {
883
+ type: "string",
884
+ enum: ["all", "registry", "context", "client"],
885
+ description: "What to generate (default: all)",
886
+ default: "all",
887
+ },
888
+ },
889
+ },
890
+ },
891
+ {
892
+ name: "run_codegen",
893
+ description: "Run CLI codegen commands (generate, generate-client).",
894
+ inputSchema: {
895
+ type: "object" as const,
896
+ properties: {
897
+ command: {
898
+ type: "string",
899
+ enum: ["generate", "generate-client", "generate-types"],
900
+ description: "Which codegen command to run",
901
+ },
902
+ outputPath: {
903
+ type: "string",
904
+ description: "Output path for generated files (optional)",
905
+ },
906
+ },
907
+ required: ["command"],
908
+ },
909
+ },
910
+ {
911
+ name: "generate_client",
912
+ description: "Generate a fully-typed API client from routes. For SvelteKit projects, generates a unified client that supports SSR direct calls (no HTTP overhead) and browser HTTP calls.",
913
+ inputSchema: {
914
+ type: "object" as const,
915
+ properties: {
916
+ outputPath: {
917
+ type: "string",
918
+ description: "Output path for generated client (e.g., 'src/lib/api.ts' for SvelteKit, './client' for standalone)",
919
+ },
920
+ },
921
+ },
922
+ },
923
+ ];
924
+
925
+ // =============================================================================
926
+ // TOOL IMPLEMENTATIONS
927
+ // =============================================================================
928
+
929
+ async function getProjectInfo(): Promise<string> {
930
+ return await generateProjectAnalysis();
931
+ }
932
+
933
+ async function getArchitectureGuidance(args: { task: string }): Promise<string> {
934
+ const { task } = args;
935
+ const taskLower = task.toLowerCase();
936
+
937
+ const plugins = listAvailablePlugins();
938
+ const routers = listAvailableRouters();
939
+
940
+ let guidance = `# Architecture Guidance\n\n**Task:** ${task}\n\n`;
941
+
942
+ // Pattern matching for common tasks
943
+ if (taskLower.includes("auth") || taskLower.includes("login") || taskLower.includes("user")) {
944
+ guidance += `## Recommended Approach: Authentication System\n\n`;
945
+ guidance += `### Step 1: Create Auth Plugin\n`;
946
+ guidance += `\`\`\`\nTool: create_plugin\n name: "auth"\n hasSchema: true\n\`\`\`\n\n`;
947
+
948
+ guidance += `### Step 2: Add Database Migration\n`;
949
+ guidance += `\`\`\`\nTool: add_migration\n pluginName: "auth"\n migrationName: "create_users"\n upSql: "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE, password_hash TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP)"\n\`\`\`\n\n`;
950
+
951
+ guidance += `### Step 3: Add Service Methods\n`;
952
+ guidance += `Add these methods using \`add_service_method\`:\n`;
953
+ guidance += `- \`createUser(email, password)\` - Create new user\n`;
954
+ guidance += `- \`validateCredentials(email, password)\` - Check login\n`;
955
+ guidance += `- \`generateToken(userId)\` - Create JWT/session\n`;
956
+ guidance += `- \`validateToken(token)\` - Verify token\n\n`;
957
+
958
+ guidance += `### Step 4: Add Auth Middleware\n`;
959
+ guidance += `\`\`\`\nTool: extend_plugin\n pluginName: "auth"\n extensionType: "middleware"\n name: "authRequired"\n\`\`\`\n\n`;
960
+
961
+ guidance += `### Step 5: Create Auth Routes\n`;
962
+ guidance += `\`\`\`\nTool: create_router\n routerPath: "${projectConfig.routesDir}/auth/index.ts"\n routerName: "authRouter"\n prefix: "auth"\n\`\`\`\n\n`;
963
+ guidance += `Then add routes: login, register, logout, me\n\n`;
964
+
965
+ } else if (taskLower.includes("crud") || taskLower.includes("api") || taskLower.includes("resource")) {
966
+ const resourceName = extractResourceName(task);
967
+
968
+ guidance += `## Recommended Approach: CRUD API for "${resourceName}"\n\n`;
969
+
970
+ guidance += `### Step 1: Create Plugin (Business Logic)\n`;
971
+ guidance += `\`\`\`\nTool: create_plugin\n name: "${resourceName}"\n hasSchema: true\n\`\`\`\n\n`;
972
+
973
+ guidance += `### Step 2: Add Database Migration\n`;
974
+ guidance += `\`\`\`\nTool: add_migration\n pluginName: "${resourceName}"\n migrationName: "create_${resourceName}"\n\`\`\`\n\n`;
975
+
976
+ guidance += `### Step 3: Add Service Methods\n`;
977
+ guidance += `Using \`add_service_method\`, add:\n`;
978
+ guidance += `- \`list(options)\` - List with pagination/filtering\n`;
979
+ guidance += `- \`getById(id)\` - Get single item\n`;
980
+ guidance += `- \`create(data)\` - Create new item\n`;
981
+ guidance += `- \`update(id, data)\` - Update existing\n`;
982
+ guidance += `- \`delete(id)\` - Delete item\n\n`;
983
+
984
+ guidance += `### Step 4: Create Router\n`;
985
+ guidance += `\`\`\`\nTool: create_router\n routerPath: "${projectConfig.routesDir}/${toKebabCase(resourceName)}/index.ts"\n routerName: "${resourceName}Router"\n prefix: "${resourceName}"\n\`\`\`\n\n`;
986
+
987
+ guidance += `### Step 5: Add Routes\n`;
988
+ guidance += `Using \`add_route\`, add these routes:\n`;
989
+ guidance += `- list (GET all)\n- get (GET by ID)\n- create (POST)\n- update (PUT)\n- delete (DELETE)\n\n`;
990
+
991
+ } else if (taskLower.includes("realtime") || taskLower.includes("sse") || taskLower.includes("live")) {
992
+ guidance += `## Recommended Approach: Real-time Updates with SSE\n\n`;
993
+
994
+ guidance += `### Step 1: Create Plugin for State Management\n`;
995
+ guidance += `\`\`\`\nTool: create_plugin\n name: "realtime"\n\`\`\`\n\n`;
996
+
997
+ guidance += `### Step 2: Add Event Types\n`;
998
+ guidance += `\`\`\`\nTool: add_event\n pluginName: "realtime"\n name: "update"\n schema: "z.object({ type: z.string(), data: z.any() })"\n\`\`\`\n\n`;
999
+
1000
+ guidance += `### Step 3: Add SSE Route\n`;
1001
+ guidance += `\`\`\`\nTool: add_sse_route\n routerFile: "${projectConfig.routesDir}/realtime/index.ts"\n routeName: "subscribe"\n channel: "updates"\n\`\`\`\n\n`;
1002
+
1003
+ guidance += `### Step 4: Broadcast Updates\n`;
1004
+ guidance += `In your service methods, use:\n`;
1005
+ guidance += `\`\`\`typescript\nctx.core.sse.broadcast("updates", "event-name", { data });\n\`\`\`\n\n`;
1006
+
1007
+ } else if (taskLower.includes("cron") || taskLower.includes("schedule") || taskLower.includes("periodic")) {
1008
+ guidance += `## Recommended Approach: Scheduled Tasks\n\n`;
1009
+
1010
+ guidance += `### Step 1: Add to Existing Plugin or Create New One\n`;
1011
+ guidance += `Cron jobs should belong to a plugin that owns the related business logic.\n\n`;
1012
+
1013
+ guidance += `### Step 2: Add Cron Job\n`;
1014
+ guidance += `\`\`\`\nTool: add_cron\n pluginName: "<your-plugin>"\n name: "daily-cleanup"\n schedule: "0 0 * * *" // Daily at midnight\n implementation: "// Your task code"\n\`\`\`\n\n`;
1015
+
1016
+ guidance += `### Common Cron Schedules:\n`;
1017
+ guidance += `- \`* * * * *\` - Every minute\n`;
1018
+ guidance += `- \`0 * * * *\` - Every hour\n`;
1019
+ guidance += `- \`0 0 * * *\` - Daily at midnight\n`;
1020
+ guidance += `- \`0 0 * * 0\` - Weekly on Sunday\n`;
1021
+ guidance += `- \`0 0 1 * *\` - Monthly on 1st\n\n`;
1022
+
1023
+ } else if (taskLower.includes("job") || taskLower.includes("background") || taskLower.includes("queue")) {
1024
+ guidance += `## Recommended Approach: Background Jobs\n\n`;
1025
+
1026
+ guidance += `### Step 1: Register Job Handler in Plugin\n`;
1027
+ guidance += `\`\`\`\nTool: add_async_job\n pluginName: "<your-plugin>"\n name: "process-order"\n implementation: "// Job handler code"\n\`\`\`\n\n`;
1028
+
1029
+ guidance += `### Step 2: Enqueue Jobs from Service Methods\n`;
1030
+ guidance += `\`\`\`typescript\nawait ctx.core.jobs.enqueue("process-order", { orderId: "123" });\n\`\`\`\n\n`;
1031
+
1032
+ guidance += `### Features:\n`;
1033
+ guidance += `- Automatic retries with configurable attempts\n`;
1034
+ guidance += `- Delayed execution with scheduling\n`;
1035
+ guidance += `- Job status tracking\n`;
1036
+ guidance += `- Event emission on completion/failure\n\n`;
1037
+
1038
+ } else {
1039
+ guidance += `## General Approach\n\n`;
1040
+ guidance += `1. **Identify the domain** - What entity/concept are you working with?\n`;
1041
+ guidance += `2. **Create a plugin** for business logic using \`create_plugin\`\n`;
1042
+ guidance += `3. **Add database schema** if needed using \`add_migration\`\n`;
1043
+ guidance += `4. **Add service methods** for business logic using \`add_service_method\`\n`;
1044
+ guidance += `5. **Create routes** to expose the functionality using \`create_router\` and \`add_route\`\n`;
1045
+ guidance += `6. **Run type generation** using \`generate_types\`\n`;
1046
+ guidance += `7. **Generate API client** using \`generate_client\` for typed frontend calls\n\n`;
1047
+ }
1048
+
1049
+ // Current project state
1050
+ guidance += `## Current Project State\n\n`;
1051
+ guidance += `- **Plugins:** ${plugins.length > 0 ? plugins.join(", ") : "none"}\n`;
1052
+ guidance += `- **Router files:** ${routers.length}\n`;
1053
+ if (projectConfig.adapter) {
1054
+ guidance += `- **Adapter:** ${projectConfig.adapter}\n`;
1055
+ }
1056
+
1057
+ // Warnings
1058
+ if (taskLower.includes("protected") || taskLower.includes("secure")) {
1059
+ if (!plugins.includes("auth")) {
1060
+ guidance += `\n### Warning\n`;
1061
+ guidance += `No auth plugin detected. Create one first if you need protected routes.\n`;
1062
+ }
1063
+ }
1064
+
1065
+ // Client generation reminder
1066
+ guidance += `\n## Final Step: Generate API Client\n`;
1067
+ guidance += `After creating routes, generate a typed client:\n`;
1068
+ guidance += `\`\`\`\nTool: generate_client\n\`\`\`\n`;
1069
+ if (projectConfig.adapter === "sveltekit") {
1070
+ guidance += `\n**SvelteKit Benefit:** The generated client supports:\n`;
1071
+ guidance += `- **SSR:** Direct calls via \`locals\` (no HTTP overhead!)\n`;
1072
+ guidance += `- **Browser:** HTTP calls automatically\n`;
1073
+ }
1074
+
1075
+ guidance += `\n## Documentation Resources\n`;
1076
+ guidance += `- \`donkeylabs://docs/plugins\` - Plugin patterns\n`;
1077
+ guidance += `- \`donkeylabs://docs/router\` - Route patterns\n`;
1078
+ guidance += `- \`donkeylabs://docs/api-client\` - Client usage & generation\n`;
1079
+ guidance += `- \`donkeylabs://docs/project-structure\` - Best practices\n`;
1080
+
1081
+ return guidance;
1082
+ }
1083
+
1084
+ function extractResourceName(task: string): string {
1085
+ // Try to extract a resource name from the task description
1086
+ const patterns = [
1087
+ /crud\s+(?:for\s+)?(\w+)/i,
1088
+ /api\s+(?:for\s+)?(\w+)/i,
1089
+ /(\w+)\s+api/i,
1090
+ /(\w+)\s+crud/i,
1091
+ /manage\s+(\w+)/i,
1092
+ /(\w+)\s+management/i,
1093
+ ];
1094
+
1095
+ for (const pattern of patterns) {
1096
+ const match = task.match(pattern);
1097
+ if (match) {
1098
+ return match[1].toLowerCase();
1099
+ }
1100
+ }
1101
+
1102
+ return "items";
1103
+ }
1104
+
1105
+ async function createPlugin(args: {
1106
+ name: string;
1107
+ hasSchema?: boolean;
1108
+ hasConfig?: boolean;
1109
+ configFields?: string;
1110
+ dependencies?: string[];
1111
+ }): Promise<string> {
1112
+ const { name, hasSchema = false, hasConfig = false, configFields = "", dependencies = [] } = args;
1113
+
1114
+ // Validation
1115
+ const projectValid = validateProjectExists();
1116
+ if (!projectValid.valid) {
1117
+ return formatError(projectValid.error!, undefined, projectValid.suggestion);
1118
+ }
1119
+
1120
+ const nameValid = validatePluginName(name);
1121
+ if (!nameValid.valid) {
1122
+ return formatError(nameValid.error!, undefined, nameValid.suggestion);
1123
+ }
1124
+
1125
+ const pluginDir = join(projectRoot, projectConfig.pluginsDir, name);
1126
+ if (existsSync(pluginDir)) {
1127
+ const existingContent = readFileSync(join(pluginDir, "index.ts"), "utf-8");
1128
+ const methods = [...existingContent.matchAll(/(\w+):\s*(?:async\s*)?\(/g)].map(m => m[1]);
1129
+
1130
+ return formatError(
1131
+ `Plugin "${name}" already exists`,
1132
+ `Location: ${projectConfig.pluginsDir}/${name}/\nExisting methods: ${methods.join(", ") || "none"}`,
1133
+ `To add to this plugin:\n- Use \`add_service_method\` to add methods\n- Use \`extend_plugin\` to add errors/events/middleware\n- Use \`add_migration\` to add database changes`,
1134
+ "add_service_method"
1135
+ );
1136
+ }
1137
+
1138
+ // Validate dependencies exist
1139
+ for (const dep of dependencies) {
1140
+ const depValid = validatePluginExists(dep);
1141
+ if (!depValid.valid) {
1142
+ return formatError(
1143
+ `Dependency plugin "${dep}" not found`,
1144
+ undefined,
1145
+ `Create the dependency plugin first, or remove it from dependencies.`,
1146
+ "create_plugin"
1147
+ );
1148
+ }
1149
+ }
1150
+
1151
+ mkdirSync(pluginDir, { recursive: true });
1152
+
1153
+ // Build the createPlugin chain
1154
+ let createPluginChain = "createPlugin";
1155
+ if (hasSchema) {
1156
+ createPluginChain += `\n .withSchema<${toPascalCase(name)}Schema>()`;
1157
+ }
1158
+ if (hasConfig) {
1159
+ createPluginChain += `\n .withConfig<${toPascalCase(name)}Config>()`;
1160
+ }
1161
+ createPluginChain += "\n .define";
1162
+
1163
+ // Generate imports
1164
+ let imports = `import { createPlugin } from "@donkeylabs/server";\n`;
1165
+ if (dependencies.length > 0) {
1166
+ imports += dependencies.map(d => `import { ${d}Plugin } from "../${d}";`).join("\n") + "\n";
1167
+ }
1168
+ if (hasSchema) {
1169
+ imports += `import type { ${toPascalCase(name)}Schema } from "./schema";\n`;
1170
+ }
1171
+
1172
+ // Generate config interface if needed
1173
+ let configInterface = "";
1174
+ if (hasConfig) {
1175
+ const fields = configFields || " // Add your config fields here\n // apiKey: string;\n // sandbox?: boolean;";
1176
+ configInterface = `\nexport interface ${toPascalCase(name)}Config {\n ${fields}\n}\n`;
1177
+ }
1178
+
1179
+ const depsArray = dependencies.length > 0
1180
+ ? `\n dependencies: [${dependencies.map(d => `${d}Plugin`).join(", ")}] as const,`
1181
+ : "";
1182
+
1183
+ // Build the service context comment based on what's available
1184
+ let ctxComment = " // ctx.plugins - access other plugin services";
1185
+ if (hasSchema) {
1186
+ ctxComment += "\n // ctx.db - typed database access (Kysely)";
1187
+ }
1188
+ if (hasConfig) {
1189
+ ctxComment += "\n // ctx.config - your plugin configuration";
1190
+ }
1191
+ if (dependencies.length > 0) {
1192
+ ctxComment += `\n // ctx.deps - dependency services: ${dependencies.join(", ")}`;
1193
+ }
1194
+
1195
+ const indexContent = `${imports}${configInterface}
1196
+ export const ${name}Plugin = ${createPluginChain}({
1197
+ name: "${name}",${depsArray}
1198
+
1199
+ service: async (ctx) => {
1200
+ ${ctxComment}
1201
+ // ctx.core - logger, cache, events, cron, jobs, sse, rateLimiter
1202
+
1203
+ return {
1204
+ // Service methods are available via ctx.plugins.${name}
1205
+ hello: () => "Hello from ${name} plugin!",
1206
+ };
1207
+ },
1208
+
1209
+ // Uncomment to add initialization logic (crons, event listeners, jobs)
1210
+ // init: (ctx, service) => {
1211
+ // // Register cron jobs
1212
+ // // ctx.core.cron.schedule("0 * * * *", async () => { ... }, { name: "hourly-task" });
1213
+ //
1214
+ // // Register async jobs
1215
+ // // ctx.core.jobs.register("job-name", async (data) => { ... });
1216
+ //
1217
+ // // Listen for events
1218
+ // // ctx.core.events.on("user.created", async (data) => { ... });
1219
+ //
1220
+ // ctx.core.logger.info("${name} plugin initialized");
1221
+ // },
1222
+ });
1223
+ `;
1224
+
1225
+ await Bun.write(join(pluginDir, "index.ts"), indexContent);
1226
+
1227
+ // Create schema and migrations if needed
1228
+ if (hasSchema) {
1229
+ mkdirSync(join(pluginDir, "migrations"), { recursive: true });
1230
+
1231
+ const schemaContent = `// Database schema types for ${name} plugin
1232
+ // Run 'donkeylabs generate' after adding migrations to generate types
1233
+
1234
+ export interface ${toPascalCase(name)}Schema {
1235
+ // Tables will be generated here
1236
+ // Example:
1237
+ // ${name}: {
1238
+ // id: Generated<number>;
1239
+ // name: string;
1240
+ // created_at: string;
1241
+ // };
1242
+ }
1243
+ `;
1244
+ await Bun.write(join(pluginDir, "schema.ts"), schemaContent);
1245
+
1246
+ const migrationContent = `-- Migration: 001_initial
1247
+ -- Created: ${new Date().toISOString()}
1248
+ -- Plugin: ${name}
1249
+
1250
+ -- UP
1251
+ -- Add your CREATE TABLE statements here
1252
+ -- Example:
1253
+ -- CREATE TABLE ${name} (
1254
+ -- id INTEGER PRIMARY KEY AUTOINCREMENT,
1255
+ -- name TEXT NOT NULL,
1256
+ -- created_at TEXT DEFAULT CURRENT_TIMESTAMP
1257
+ -- );
1258
+
1259
+ -- DOWN
1260
+ -- Add your DROP TABLE statements here
1261
+ -- DROP TABLE IF EXISTS ${name};
1262
+ `;
1263
+ await Bun.write(join(pluginDir, "migrations", "001_initial.sql"), migrationContent);
1264
+ }
1265
+
1266
+ // Build registration example based on plugin type
1267
+ let registrationExample = "";
1268
+ if (hasConfig) {
1269
+ registrationExample = `\`\`\`typescript
1270
+ // server.ts
1271
+ server.registerPlugin(${name}Plugin({
1272
+ // Your config here
1273
+ }));
1274
+ \`\`\``;
1275
+ } else {
1276
+ registrationExample = `\`\`\`typescript
1277
+ // server.ts
1278
+ server.registerPlugin(${name}Plugin);
1279
+ \`\`\``;
1280
+ }
1281
+
1282
+ return `## Plugin Created: ${name}
1283
+
1284
+ **Location:** ${projectConfig.pluginsDir}/${name}/
1285
+ **Files created:**
1286
+ - index.ts (plugin definition)${hasSchema ? "\n- schema.ts (database types)\n- migrations/001_initial.sql" : ""}
1287
+
1288
+ **Plugin features:**
1289
+ ${hasSchema ? "- ✅ Database schema (withSchema<>)\n" : ""}${hasConfig ? "- ✅ Configurable (withConfig<>)\n" : ""}- Service with ctx.core (logger, cache, events, cron, jobs, sse, rateLimiter)
1290
+ - Init hook for startup logic (crons, jobs, event listeners)
1291
+
1292
+ ### Register Plugin
1293
+
1294
+ ${registrationExample}
1295
+
1296
+ ### Next Steps
1297
+
1298
+ 1. ${hasSchema ? "Edit the migration file with your schema" : "Add service methods using `add_service_method`"}
1299
+ 2. ${hasSchema ? "Run \`donkeylabs generate\` to create types" : "Register the plugin in your server"}
1300
+ 3. Use \`extend_plugin\` to add errors, events, or middleware
1301
+
1302
+ ### Example Usage
1303
+
1304
+ \`\`\`typescript
1305
+ // In your route handler:
1306
+ const result = ctx.plugins.${name}.hello();
1307
+ \`\`\`
1308
+ `;
1309
+ }
1310
+
1311
+ async function addServiceMethod(args: {
1312
+ pluginName: string;
1313
+ methodName: string;
1314
+ params?: string;
1315
+ returnType?: string;
1316
+ implementation: string;
1317
+ }): Promise<string> {
1318
+ const { pluginName, methodName, params = "", returnType = "void", implementation } = args;
1319
+
1320
+ const pluginValid = validatePluginExists(pluginName);
1321
+ if (!pluginValid.valid) {
1322
+ return formatError(pluginValid.error!, undefined, pluginValid.suggestion, "create_plugin");
1323
+ }
1324
+
1325
+ const pluginFile = join(projectRoot, projectConfig.pluginsDir, pluginName, "index.ts");
1326
+ const content = await Bun.file(pluginFile).text();
1327
+
1328
+ // Check if method already exists
1329
+ if (content.includes(`${methodName}:`)) {
1330
+ return formatError(
1331
+ `Method "${methodName}" already exists in ${pluginName} plugin`,
1332
+ undefined,
1333
+ "Choose a different method name or edit the existing method directly."
1334
+ );
1335
+ }
1336
+
1337
+ // Find the service return object
1338
+ const serviceMatch = content.match(/service:\s*async\s*\([^)]*\)\s*=>\s*\{[\s\S]*?return\s*\{/);
1339
+ if (!serviceMatch) {
1340
+ // Try simpler pattern for arrow function style
1341
+ const simpleMatch = content.match(/service:\s*async\s*\([^)]*\)\s*=>\s*\(\{/);
1342
+ if (!simpleMatch) {
1343
+ return formatError(
1344
+ "Could not find service definition",
1345
+ `File: ${projectConfig.pluginsDir}/${pluginName}/index.ts`,
1346
+ "Make sure the plugin has a service property with a return statement."
1347
+ );
1348
+ }
1349
+
1350
+ const insertPoint = simpleMatch.index! + simpleMatch[0].length;
1351
+ const methodDef = `
1352
+ ${methodName}: ${params ? `async (${params})` : "async ()"}: ${returnType.includes("Promise") ? returnType : `Promise<${returnType}>`} => {
1353
+ ${implementation}
1354
+ },`;
1355
+
1356
+ const newContent = content.slice(0, insertPoint) + methodDef + content.slice(insertPoint);
1357
+ await Bun.write(pluginFile, newContent);
1358
+ } else {
1359
+ const insertPoint = serviceMatch.index! + serviceMatch[0].length;
1360
+ const methodDef = `
1361
+ ${methodName}: ${params ? `async (${params})` : "async ()"}: ${returnType.includes("Promise") ? returnType : `Promise<${returnType}>`} => {
1362
+ ${implementation}
1363
+ },`;
1364
+
1365
+ const newContent = content.slice(0, insertPoint) + methodDef + content.slice(insertPoint);
1366
+ await Bun.write(pluginFile, newContent);
1367
+ }
1368
+
1369
+ return `## Method Added: ${methodName}
1370
+
1371
+ **Plugin:** ${pluginName}
1372
+ **Signature:** \`${methodName}(${params}): ${returnType}\`
1373
+
1374
+ ### Usage
1375
+
1376
+ \`\`\`typescript
1377
+ // In route handlers or other plugins:
1378
+ const result = await ctx.plugins.${pluginName}.${methodName}(${params ? "..." : ""});
1379
+ \`\`\`
1380
+
1381
+ **Reminder:** Run \`donkeylabs generate\` to update types.
1382
+ `;
1383
+ }
1384
+
1385
+ async function addMigration(args: {
1386
+ pluginName: string;
1387
+ migrationName: string;
1388
+ upSql: string;
1389
+ downSql?: string;
1390
+ }): Promise<string> {
1391
+ const { pluginName, migrationName, upSql, downSql = "" } = args;
1392
+
1393
+ const pluginValid = validatePluginExists(pluginName);
1394
+ if (!pluginValid.valid) {
1395
+ return formatError(pluginValid.error!, undefined, pluginValid.suggestion, "create_plugin");
1396
+ }
1397
+
1398
+ const migrationsDir = join(projectRoot, projectConfig.pluginsDir, pluginName, "migrations");
1399
+
1400
+ if (!existsSync(migrationsDir)) {
1401
+ mkdirSync(migrationsDir, { recursive: true });
1402
+ }
1403
+
1404
+ // Find next migration number
1405
+ const existing = readdirSync(migrationsDir)
1406
+ .filter((f) => f.endsWith(".sql"))
1407
+ .map((f) => parseInt(f.split("_")[0], 10))
1408
+ .filter((n) => !isNaN(n));
1409
+
1410
+ const nextNum = existing.length > 0 ? Math.max(...existing) + 1 : 1;
1411
+ const numStr = String(nextNum).padStart(3, "0");
1412
+ const filename = `${numStr}_${migrationName}.sql`;
1413
+
1414
+ const content = `-- Migration: ${numStr}_${migrationName}
1415
+ -- Created: ${new Date().toISOString()}
1416
+ -- Plugin: ${pluginName}
1417
+
1418
+ -- UP
1419
+ ${upSql}
1420
+
1421
+ -- DOWN
1422
+ ${downSql || "-- Add rollback SQL here"}
1423
+ `;
1424
+
1425
+ await Bun.write(join(migrationsDir, filename), content);
1426
+
1427
+ return `## Migration Created: ${filename}
1428
+
1429
+ **Plugin:** ${pluginName}
1430
+ **Path:** ${projectConfig.pluginsDir}/${pluginName}/migrations/${filename}
1431
+
1432
+ ### Next Steps
1433
+
1434
+ 1. Review the migration SQL
1435
+ 2. Run \`donkeylabs generate\` to update schema types
1436
+ 3. The migration will run automatically on server start
1437
+
1438
+ ### Note
1439
+ Make sure to update the plugin's schema.ts file with the new table types.
1440
+ `;
1441
+ }
1442
+
1443
+ async function createRouter(args: {
1444
+ routerPath: string;
1445
+ routerName: string;
1446
+ prefix: string;
1447
+ }): Promise<string> {
1448
+ const { routerPath, routerName, prefix } = args;
1449
+
1450
+ const projectValid = validateProjectExists();
1451
+ if (!projectValid.valid) {
1452
+ return formatError(projectValid.error!, undefined, projectValid.suggestion);
1453
+ }
1454
+
1455
+ // Validate path - must be in the configured routes directory
1456
+ if (!routerPath.includes(projectConfig.routesDir)) {
1457
+ return formatError(
1458
+ "Invalid router path",
1459
+ `Path: ${routerPath}`,
1460
+ `Router files should be in ${projectConfig.routesDir}/ directory.\nSuggested path: ${projectConfig.routesDir}/${prefix}/index.ts`
1461
+ );
1462
+ }
1463
+
1464
+ const fullPath = join(projectRoot, routerPath);
1465
+
1466
+ if (existsSync(fullPath)) {
1467
+ return formatError(
1468
+ `Router file already exists`,
1469
+ `Path: ${routerPath}`,
1470
+ "Use add_route to add routes to the existing router.",
1471
+ "add_route"
1472
+ );
1473
+ }
1474
+
1475
+ // Create directory if needed
1476
+ const routerDir = dirname(fullPath);
1477
+ if (!existsSync(routerDir)) {
1478
+ mkdirSync(routerDir, { recursive: true });
1479
+ }
1480
+
1481
+ const content = `import { createRouter } from "@donkeylabs/server";
1482
+ import { z } from "zod";
1483
+
1484
+ export const ${routerName} = createRouter("${prefix}");
1485
+
1486
+ // Add routes using the add_route tool or manually:
1487
+ // ${routerName}.route("list").typed({
1488
+ // input: z.object({ page: z.number().default(1) }),
1489
+ // handle: async (input, ctx) => {
1490
+ // return { items: [], page: input.page };
1491
+ // },
1492
+ // });
1493
+ `;
1494
+
1495
+ await Bun.write(fullPath, content);
1496
+
1497
+ return `## Router Created: ${routerName}
1498
+
1499
+ **Path:** ${routerPath}
1500
+ **Prefix:** ${prefix} (routes will be named "${prefix}.routeName")
1501
+
1502
+ ### Next Steps
1503
+
1504
+ 1. Use \`add_route\` to add routes to this router
1505
+ 2. Register the router in your server entry point:
1506
+
1507
+ \`\`\`typescript
1508
+ import { ${routerName} } from "./${relative(join(projectRoot, "src"), fullPath).replace(/\.ts$/, "")}";
1509
+ server.use(${routerName});
1510
+ \`\`\`
1511
+
1512
+ ### Route Naming
1513
+
1514
+ Routes added to this router will be named \`${prefix}.<routeName>\`:
1515
+ - ${prefix}.list
1516
+ - ${prefix}.get
1517
+ - ${prefix}.create
1518
+ `;
1519
+ }
1520
+
1521
+ async function addRoute(args: {
1522
+ routerFile: string;
1523
+ routeName: string;
1524
+ inputSchema?: string;
1525
+ outputType?: string;
1526
+ handler: string;
1527
+ useClassHandler?: boolean;
1528
+ }): Promise<string> {
1529
+ const { routerFile, routeName, inputSchema, outputType, handler, useClassHandler = true } = args;
1530
+
1531
+ const routerValid = validateRouterExists(routerFile);
1532
+ if (!routerValid.valid) {
1533
+ return formatError(routerValid.error!, undefined, routerValid.suggestion, "create_router");
1534
+ }
1535
+
1536
+ const fullPath = join(projectRoot, routerFile);
1537
+ const content = await Bun.file(fullPath).text();
1538
+
1539
+ // Check if route already exists
1540
+ if (content.includes(`.route("${routeName}")`)) {
1541
+ return formatError(
1542
+ `Route "${routeName}" already exists`,
1543
+ `File: ${routerFile}`,
1544
+ "Choose a different route name or edit the existing route directly."
1545
+ );
1546
+ }
1547
+
1548
+ const routerMatch = content.match(/createRouter\([^)]+\)/);
1549
+ if (!routerMatch) {
1550
+ return formatError(
1551
+ "Could not find createRouter() in file",
1552
+ `File: ${routerFile}`,
1553
+ "Make sure this is a valid router file with createRouter()."
1554
+ );
1555
+ }
1556
+
1557
+ let finalHandlerCode = "";
1558
+ let importStatement = "";
1559
+ let handlerFilePath = "";
1560
+
1561
+ if (useClassHandler) {
1562
+ const handlerName = toPascalCase(routeName);
1563
+ const handlerClassName = `${handlerName}Handler`;
1564
+ const handlerFileName = toKebabCase(routeName);
1565
+
1566
+ const routerDir = dirname(fullPath);
1567
+ const handlersDir = join(routerDir, "handlers");
1568
+
1569
+ if (!existsSync(handlersDir)) {
1570
+ mkdirSync(handlersDir, { recursive: true });
1571
+ }
1572
+
1573
+ // Extract router prefix for proper type path
1574
+ const prefixMatch = content.match(/createRouter\(["']([^"']+)["']\)/);
1575
+ const prefix = prefixMatch ? toPascalCase(prefixMatch[1]) : "Api";
1576
+
1577
+ // Determine import path based on project type
1578
+ const apiImportPath = projectConfig.adapter === "sveltekit" ? "$lib/api" : "@/api";
1579
+
1580
+ const handlerFileContent = `import type { Handler, AppContext, Routes } from "${apiImportPath}";
1581
+
1582
+ /**
1583
+ * Handler for ${routeName} route
1584
+ */
1585
+ export class ${handlerClassName} implements Handler<Routes.${prefix}.${handlerName}> {
1586
+ ctx: AppContext;
1587
+
1588
+ constructor(ctx: AppContext) {
1589
+ this.ctx = ctx;
1590
+ }
1591
+
1592
+ async handle(input: Routes.${prefix}.${handlerName}["input"]): Promise<Routes.${prefix}.${handlerName}["output"]> {
1593
+ ${handler}
1594
+ }
1595
+ }
1596
+ `;
1597
+
1598
+ handlerFilePath = join(handlersDir, `${handlerFileName}.ts`);
1599
+
1600
+ if (existsSync(handlerFilePath)) {
1601
+ return formatError(
1602
+ `Handler file already exists`,
1603
+ `Path: handlers/${handlerFileName}.ts`,
1604
+ "Use a different route name or use add_handler_to_router for existing handlers.",
1605
+ "add_handler_to_router"
1606
+ );
1607
+ }
1608
+
1609
+ await Bun.write(handlerFilePath, handlerFileContent);
1610
+
1611
+ finalHandlerCode = handlerClassName;
1612
+ importStatement = `import { ${handlerClassName} } from "./handlers/${handlerFileName}";\n`;
1613
+ } else {
1614
+ finalHandlerCode = `async (input, ctx) => {
1615
+ ${handler}
1616
+ }`;
1617
+ }
1618
+
1619
+ const newRoute = inputSchema
1620
+ ? `\n .route("${routeName}").typed({
1621
+ input: ${inputSchema},${outputType ? `\n output: ${outputType},` : ""}
1622
+ handle: ${finalHandlerCode},
1623
+ })`
1624
+ : `\n .route("${routeName}").typed({
1625
+ handle: ${finalHandlerCode},
1626
+ })`;
1627
+
1628
+ // Update content
1629
+ let updatedContent = content;
1630
+
1631
+ if (useClassHandler) {
1632
+ const lastImportIdx = updatedContent.lastIndexOf("import ");
1633
+ if (lastImportIdx !== -1) {
1634
+ const endOfLine = updatedContent.indexOf("\n", lastImportIdx);
1635
+ if (endOfLine !== -1) {
1636
+ updatedContent = updatedContent.slice(0, endOfLine + 1) + importStatement + updatedContent.slice(endOfLine + 1);
1637
+ }
1638
+ } else {
1639
+ updatedContent = importStatement + updatedContent;
1640
+ }
1641
+ }
1642
+
1643
+ // Find the last semicolon or closing of router chain
1644
+ const insertPoint = updatedContent.lastIndexOf(";");
1645
+ if (insertPoint !== -1) {
1646
+ updatedContent = updatedContent.slice(0, insertPoint) + newRoute + updatedContent.slice(insertPoint);
1647
+ }
1648
+
1649
+ await Bun.write(fullPath, updatedContent);
1650
+
1651
+ return `## Route Added: ${routeName}
1652
+
1653
+ **Router:** ${routerFile}${useClassHandler ? `\n**Handler:** handlers/${toKebabCase(routeName)}.ts` : ""}
1654
+
1655
+ ### Usage
1656
+
1657
+ This route is now available at: \`POST /<prefix>.${routeName}\`
1658
+
1659
+ ${inputSchema ? `**Input:** ${inputSchema}` : "**Input:** none"}
1660
+ ${outputType ? `\n**Output:** ${outputType}` : ""}
1661
+
1662
+ ### Next Steps
1663
+
1664
+ 1. ${useClassHandler ? "Implement the handler logic in the handler file" : "The route is ready to use"}
1665
+ 2. Run \`donkeylabs generate\` to update types
1666
+ 3. Test the route
1667
+ `;
1668
+ }
1669
+
1670
+ async function addHandlerToRouter(args: {
1671
+ routerFile: string;
1672
+ handlerName: string;
1673
+ handlerPath: string;
1674
+ routeName: string;
1675
+ inputSchema?: string;
1676
+ outputSchema?: string;
1677
+ }): Promise<string> {
1678
+ const { routerFile, handlerName, handlerPath, routeName, inputSchema, outputSchema } = args;
1679
+
1680
+ const routerValid = validateRouterExists(routerFile);
1681
+ if (!routerValid.valid) {
1682
+ return formatError(routerValid.error!, undefined, routerValid.suggestion, "create_router");
1683
+ }
1684
+
1685
+ const fullPath = join(projectRoot, routerFile);
1686
+ let content = await Bun.file(fullPath).text();
1687
+
1688
+ // Add import
1689
+ const importStatement = `import { ${handlerName} } from "${handlerPath}";\n`;
1690
+
1691
+ if (!content.includes(importStatement)) {
1692
+ const lastImportIdx = content.lastIndexOf("import ");
1693
+ if (lastImportIdx !== -1) {
1694
+ const endOfLine = content.indexOf("\n", lastImportIdx);
1695
+ if (endOfLine !== -1) {
1696
+ content = content.slice(0, endOfLine + 1) + importStatement + content.slice(endOfLine + 1);
1697
+ }
1698
+ } else {
1699
+ content = importStatement + content;
1700
+ }
1701
+ }
1702
+
1703
+ // Add route
1704
+ const routeDef = inputSchema
1705
+ ? `\n .route("${routeName}").typed({
1706
+ input: ${inputSchema},${outputSchema ? `\n output: ${outputSchema},` : ""}
1707
+ handle: ${handlerName},
1708
+ })`
1709
+ : `\n .route("${routeName}").typed({
1710
+ handle: ${handlerName},
1711
+ })`;
1712
+
1713
+ const insertPoint = content.lastIndexOf(";");
1714
+ if (insertPoint !== -1) {
1715
+ content = content.slice(0, insertPoint) + routeDef + content.slice(insertPoint);
1716
+ }
1717
+
1718
+ await Bun.write(fullPath, content);
1719
+
1720
+ return `## Handler Registered: ${handlerName}
1721
+
1722
+ **Route:** ${routeName}
1723
+ **Router:** ${routerFile}
1724
+ **Handler import:** ${handlerPath}
1725
+
1726
+ Run \`donkeylabs generate\` to update types.
1727
+ `;
1728
+ }
1729
+
1730
+ async function extendPlugin(args: {
1731
+ pluginName: string;
1732
+ extensionType: "error" | "event" | "middleware" | "handler";
1733
+ name: string;
1734
+ params?: {
1735
+ code?: string;
1736
+ status?: number;
1737
+ message?: string;
1738
+ schema?: string;
1739
+ implementation?: string;
1740
+ handlerSignature?: string;
1741
+ };
1742
+ }): Promise<string> {
1743
+ const { pluginName, extensionType, name, params = {} } = args;
1744
+
1745
+ const pluginValid = validatePluginExists(pluginName);
1746
+ if (!pluginValid.valid) {
1747
+ return formatError(pluginValid.error!, undefined, pluginValid.suggestion, "create_plugin");
1748
+ }
1749
+
1750
+ const pluginFile = join(projectRoot, projectConfig.pluginsDir, pluginName, "index.ts");
1751
+ let content = await Bun.file(pluginFile).text();
1752
+
1753
+ if (extensionType === "error") {
1754
+ const { code = name.toUpperCase().replace(/([a-z])([A-Z])/g, "$1_$2"), status = 400, message } = params;
1755
+
1756
+ // Check if customErrors already exists
1757
+ if (content.includes("customErrors:")) {
1758
+ // Add to existing customErrors
1759
+ const errorsMatch = content.match(/customErrors:\s*\{/);
1760
+ if (errorsMatch) {
1761
+ const insertPoint = errorsMatch.index! + errorsMatch[0].length;
1762
+ const errorDef = `\n ${name}: { status: ${status}, code: "${code}"${message ? `, defaultMessage: "${message}"` : ""} },`;
1763
+ content = content.slice(0, insertPoint) + errorDef + content.slice(insertPoint);
1764
+ }
1765
+ } else {
1766
+ // Add customErrors property
1767
+ const defineMatch = content.match(/\.define\(\{/);
1768
+ if (defineMatch) {
1769
+ const insertPoint = defineMatch.index! + defineMatch[0].length;
1770
+ const errorsDef = `\n customErrors: {\n ${name}: { status: ${status}, code: "${code}"${message ? `, defaultMessage: "${message}"` : ""} },\n },`;
1771
+ content = content.slice(0, insertPoint) + errorsDef + content.slice(insertPoint);
1772
+ }
1773
+ }
1774
+
1775
+ await Bun.write(pluginFile, content);
1776
+
1777
+ return `## Custom Error Added: ${name}
1778
+
1779
+ **Plugin:** ${pluginName}
1780
+ **Code:** ${code}
1781
+ **Status:** ${status}
1782
+
1783
+ ### Usage
1784
+
1785
+ \`\`\`typescript
1786
+ throw ctx.errors.${name}("Error message");
1787
+ \`\`\`
1788
+
1789
+ Run \`donkeylabs generate\` to update types.
1790
+ `;
1791
+ }
1792
+
1793
+ if (extensionType === "event") {
1794
+ const { schema = "z.object({})" } = params;
1795
+
1796
+ // Check if events already exists
1797
+ if (content.includes("events:")) {
1798
+ const eventsMatch = content.match(/events:\s*\{/);
1799
+ if (eventsMatch) {
1800
+ const insertPoint = eventsMatch.index! + eventsMatch[0].length;
1801
+ const eventDef = `\n "${name}": ${schema},`;
1802
+ content = content.slice(0, insertPoint) + eventDef + content.slice(insertPoint);
1803
+ }
1804
+ } else {
1805
+ const defineMatch = content.match(/\.define\(\{/);
1806
+ if (defineMatch) {
1807
+ const insertPoint = defineMatch.index! + defineMatch[0].length;
1808
+ const eventsDef = `\n events: {\n "${name}": ${schema},\n },`;
1809
+ content = content.slice(0, insertPoint) + eventsDef + content.slice(insertPoint);
1810
+ }
1811
+ }
1812
+
1813
+ await Bun.write(pluginFile, content);
1814
+
1815
+ return `## Event Added: ${name}
1816
+
1817
+ **Plugin:** ${pluginName}
1818
+ **Schema:** ${schema}
1819
+
1820
+ ### Usage
1821
+
1822
+ \`\`\`typescript
1823
+ // Emit event
1824
+ await ctx.core.events.emit("${name}", { /* data */ });
1825
+
1826
+ // Listen for event (in init hook)
1827
+ ctx.core.events.on("${name}", (data) => { /* handler */ });
1828
+ \`\`\`
1829
+
1830
+ Run \`donkeylabs generate\` to update types.
1831
+ `;
1832
+ }
1833
+
1834
+ if (extensionType === "middleware") {
1835
+ const { implementation = "return next();" } = params;
1836
+
1837
+ // Check if middleware function already exists
1838
+ if (content.includes("middleware:")) {
1839
+ const middlewareMatch = content.match(/middleware:\s*\([^)]*\)\s*=>\s*\(\{/);
1840
+ if (middlewareMatch) {
1841
+ const insertPoint = middlewareMatch.index! + middlewareMatch[0].length;
1842
+ const middlewareDef = `\n ${name}: createMiddleware(async (req, ctx, next, config) => {
1843
+ ${implementation}
1844
+ }),`;
1845
+ content = content.slice(0, insertPoint) + middlewareDef + content.slice(insertPoint);
1846
+ }
1847
+ } else {
1848
+ // Add middleware property after service
1849
+ const serviceEndMatch = content.match(/service:[\s\S]*?\}\),/);
1850
+ if (serviceEndMatch) {
1851
+ const insertPoint = serviceEndMatch.index! + serviceEndMatch[0].length;
1852
+ const middlewareDef = `\n\n middleware: (ctx, service) => ({
1853
+ ${name}: createMiddleware(async (req, reqCtx, next, config) => {
1854
+ ${implementation}
1855
+ }),
1856
+ }),`;
1857
+ content = content.slice(0, insertPoint) + middlewareDef + content.slice(insertPoint);
1858
+
1859
+ // Add createMiddleware import if not present
1860
+ if (!content.includes("createMiddleware")) {
1861
+ content = content.replace(
1862
+ /import \{ createPlugin \} from/,
1863
+ "import { createPlugin, createMiddleware } from"
1864
+ );
1865
+ }
1866
+ }
1867
+ }
1868
+
1869
+ await Bun.write(pluginFile, content);
1870
+
1871
+ return `## Middleware Added: ${name}
1872
+
1873
+ **Plugin:** ${pluginName}
1874
+
1875
+ ### Usage
1876
+
1877
+ \`\`\`typescript
1878
+ // In router
1879
+ router.middleware.${name}({ /* config */ }).route("protected").typed({ ... });
1880
+ \`\`\`
1881
+
1882
+ Run \`donkeylabs generate\` to update types.
1883
+ `;
1884
+ }
1885
+
1886
+ if (extensionType === "handler") {
1887
+ const { implementation = "return new Response('OK');", handlerSignature = "(body: any, ctx: ServerContext) => Promise<Response>" } = params;
1888
+
1889
+ // Check if handlers already exists
1890
+ if (content.includes("handlers:")) {
1891
+ const handlersMatch = content.match(/handlers:\s*\{/);
1892
+ if (handlersMatch) {
1893
+ const insertPoint = handlersMatch.index! + handlersMatch[0].length;
1894
+ content = content.slice(0, insertPoint) + `\n ${name}: createHandler<${handlerSignature}>(async (req, def, handle, ctx) => {\n ${implementation}\n }),` + content.slice(insertPoint);
1895
+ }
1896
+ } else {
1897
+ // Add handlers property before service
1898
+ const serviceMatch = content.match(/service:\s*async/);
1899
+ if (serviceMatch) {
1900
+ const insertPoint = serviceMatch.index!;
1901
+ const handlerDef = `handlers: {\n ${name}: createHandler<${handlerSignature}>(async (req, def, handle, ctx) => {\n ${implementation}\n }),\n },\n\n `;
1902
+ content = content.slice(0, insertPoint) + handlerDef + content.slice(insertPoint);
1903
+ }
1904
+ }
1905
+
1906
+ // Add createHandler import if not present
1907
+ if (!content.includes("createHandler")) {
1908
+ content = content.replace(
1909
+ /import \{ createPlugin/,
1910
+ "import { createPlugin, createHandler"
1911
+ );
1912
+ }
1913
+
1914
+ await Bun.write(pluginFile, content);
1915
+
1916
+ return `## Custom Handler Added: ${name}
1917
+
1918
+ **Plugin:** ${pluginName}
1919
+
1920
+ ### Usage
1921
+
1922
+ After running \`donkeylabs generate\`, use in routes:
1923
+ \`\`\`typescript
1924
+ router.route("myRoute").${name}({
1925
+ handle: async (body, ctx) => {
1926
+ // Your handler logic
1927
+ return new Response("Result");
1928
+ }
1929
+ });
1930
+ \`\`\`
1931
+
1932
+ Run \`donkeylabs generate\` to update types.
1933
+ `;
1934
+ }
1935
+
1936
+ return `Unknown extension type: ${extensionType}`;
1937
+ }
1938
+
1939
+ async function addCron(args: {
1940
+ pluginName: string;
1941
+ name: string;
1942
+ schedule: string;
1943
+ implementation: string;
1944
+ }): Promise<string> {
1945
+ const { pluginName, name, schedule, implementation } = args;
1946
+
1947
+ const pluginValid = validatePluginExists(pluginName);
1948
+ if (!pluginValid.valid) {
1949
+ return formatError(pluginValid.error!, undefined, pluginValid.suggestion, "create_plugin");
1950
+ }
1951
+
1952
+ const pluginFile = join(projectRoot, projectConfig.pluginsDir, pluginName, "index.ts");
1953
+ let content = await Bun.file(pluginFile).text();
1954
+
1955
+ const cronCode = `ctx.core.cron.schedule("${schedule}", async () => {
1956
+ ${implementation}
1957
+ }, { name: "${name}" });`;
1958
+
1959
+ // Check if init hook exists
1960
+ if (content.includes("init:")) {
1961
+ // Add to existing init
1962
+ const initMatch = content.match(/init:\s*\([^)]*\)\s*=>\s*\{/);
1963
+ if (initMatch) {
1964
+ const insertPoint = initMatch.index! + initMatch[0].length;
1965
+ content = content.slice(0, insertPoint) + `\n ${cronCode}\n` + content.slice(insertPoint);
1966
+ }
1967
+ } else {
1968
+ // Add init hook
1969
+ const serviceEndMatch = content.match(/service:[\s\S]*?\}\),/);
1970
+ if (serviceEndMatch) {
1971
+ const insertPoint = serviceEndMatch.index! + serviceEndMatch[0].length;
1972
+ const initDef = `\n\n init: (ctx, service) => {\n ${cronCode}\n },`;
1973
+ content = content.slice(0, insertPoint) + initDef + content.slice(insertPoint);
1974
+ }
1975
+ }
1976
+
1977
+ await Bun.write(pluginFile, content);
1978
+
1979
+ return `## Cron Job Added: ${name}
1980
+
1981
+ **Plugin:** ${pluginName}
1982
+ **Schedule:** ${schedule}
1983
+
1984
+ ### Schedule Reference
1985
+
1986
+ - \`* * * * *\` - Every minute
1987
+ - \`0 * * * *\` - Every hour
1988
+ - \`0 0 * * *\` - Daily at midnight
1989
+ - \`0 0 * * 0\` - Weekly on Sunday
1990
+ - \`0 0 1 * *\` - Monthly on 1st
1991
+
1992
+ The cron job will start when the server starts.
1993
+ `;
1994
+ }
1995
+
1996
+ async function addEvent(args: {
1997
+ pluginName: string;
1998
+ name: string;
1999
+ schema: string;
2000
+ }): Promise<string> {
2001
+ return await extendPlugin({
2002
+ pluginName: args.pluginName,
2003
+ extensionType: "event",
2004
+ name: args.name,
2005
+ params: { schema: args.schema },
2006
+ });
2007
+ }
2008
+
2009
+ async function addAsyncJob(args: {
2010
+ pluginName: string;
2011
+ name: string;
2012
+ implementation: string;
2013
+ }): Promise<string> {
2014
+ const { pluginName, name, implementation } = args;
2015
+
2016
+ const pluginValid = validatePluginExists(pluginName);
2017
+ if (!pluginValid.valid) {
2018
+ return formatError(pluginValid.error!, undefined, pluginValid.suggestion, "create_plugin");
2019
+ }
2020
+
2021
+ const pluginFile = join(projectRoot, projectConfig.pluginsDir, pluginName, "index.ts");
2022
+ let content = await Bun.file(pluginFile).text();
2023
+
2024
+ const jobCode = `ctx.core.jobs.register("${name}", async (data) => {
2025
+ ${implementation}
2026
+ });`;
2027
+
2028
+ // Check if init hook exists
2029
+ if (content.includes("init:")) {
2030
+ const initMatch = content.match(/init:\s*\([^)]*\)\s*=>\s*\{/);
2031
+ if (initMatch) {
2032
+ const insertPoint = initMatch.index! + initMatch[0].length;
2033
+ content = content.slice(0, insertPoint) + `\n ${jobCode}\n` + content.slice(insertPoint);
2034
+ }
2035
+ } else {
2036
+ const serviceEndMatch = content.match(/service:[\s\S]*?\}\),/);
2037
+ if (serviceEndMatch) {
2038
+ const insertPoint = serviceEndMatch.index! + serviceEndMatch[0].length;
2039
+ const initDef = `\n\n init: (ctx, service) => {\n ${jobCode}\n },`;
2040
+ content = content.slice(0, insertPoint) + initDef + content.slice(insertPoint);
2041
+ }
2042
+ }
2043
+
2044
+ await Bun.write(pluginFile, content);
2045
+
2046
+ return `## Background Job Added: ${name}
2047
+
2048
+ **Plugin:** ${pluginName}
2049
+
2050
+ ### Usage
2051
+
2052
+ \`\`\`typescript
2053
+ // Enqueue a job
2054
+ await ctx.core.jobs.enqueue("${name}", { /* data */ });
2055
+
2056
+ // Schedule for later
2057
+ await ctx.core.jobs.schedule("${name}", { data }, new Date(Date.now() + 3600000));
2058
+ \`\`\`
2059
+
2060
+ Jobs are processed automatically with retries.
2061
+ `;
2062
+ }
2063
+
2064
+ async function addSSERoute(args: {
2065
+ routerFile: string;
2066
+ routeName: string;
2067
+ channel: string;
2068
+ }): Promise<string> {
2069
+ const { routerFile, routeName, channel } = args;
2070
+
2071
+ const routerValid = validateRouterExists(routerFile);
2072
+ if (!routerValid.valid) {
2073
+ return formatError(routerValid.error!, undefined, routerValid.suggestion, "create_router");
2074
+ }
2075
+
2076
+ const fullPath = join(projectRoot, routerFile);
2077
+ let content = await Bun.file(fullPath).text();
2078
+
2079
+ const sseDef = `\n .route("${routeName}").raw({
2080
+ handle: async (req, ctx) => {
2081
+ const { client, response } = ctx.core.sse.addClient();
2082
+ ctx.core.sse.subscribe(client.id, "${channel}");
2083
+ return response;
2084
+ },
2085
+ })`;
2086
+
2087
+ const insertPoint = content.lastIndexOf(";");
2088
+ if (insertPoint !== -1) {
2089
+ content = content.slice(0, insertPoint) + sseDef + content.slice(insertPoint);
2090
+ }
2091
+
2092
+ await Bun.write(fullPath, content);
2093
+
2094
+ return `## SSE Route Added: ${routeName}
2095
+
2096
+ **Router:** ${routerFile}
2097
+ **Channel:** ${channel}
2098
+
2099
+ ### Client Usage
2100
+
2101
+ \`\`\`javascript
2102
+ const eventSource = new EventSource("/<prefix>.${routeName}");
2103
+ eventSource.onmessage = (event) => {
2104
+ console.log("Received:", event.data);
2105
+ };
2106
+ \`\`\`
2107
+
2108
+ ### Server Broadcasting
2109
+
2110
+ \`\`\`typescript
2111
+ ctx.core.sse.broadcast("${channel}", "event-name", { data });
2112
+ \`\`\`
2113
+ `;
2114
+ }
2115
+
2116
+ async function listPlugins(): Promise<string> {
2117
+ const pluginsDir = join(projectRoot, projectConfig.pluginsDir);
2118
+
2119
+ if (!existsSync(pluginsDir)) {
2120
+ return `No plugins directory found at ${projectConfig.pluginsDir}/. Create a plugin using \`create_plugin\` tool.`;
2121
+ }
2122
+
2123
+ const plugins: string[] = [];
2124
+
2125
+ for (const entry of readdirSync(pluginsDir)) {
2126
+ const pluginPath = join(pluginsDir, entry);
2127
+ if (!statSync(pluginPath).isDirectory()) continue;
2128
+
2129
+ const indexPath = join(pluginPath, "index.ts");
2130
+ if (!existsSync(indexPath)) continue;
2131
+
2132
+ const content = await Bun.file(indexPath).text();
2133
+
2134
+ // Extract service methods - look for method patterns like `methodName: async (...` or `methodName: (...`
2135
+ const methods: string[] = [];
2136
+
2137
+ // Find the return statement in service
2138
+ const serviceReturnMatch = content.match(/service:\s*async\s*\([^)]*\)\s*=>\s*\{[\s\S]*?return\s*\{([\s\S]*?)\};?\s*\},/);
2139
+ if (serviceReturnMatch) {
2140
+ // Extract method names from return object
2141
+ const returnBlock = serviceReturnMatch[1];
2142
+ const methodMatches = returnBlock.matchAll(/(\w+):\s*(?:async\s*)?\(/g);
2143
+ for (const m of methodMatches) {
2144
+ if (!methods.includes(m[1])) {
2145
+ methods.push(m[1]);
2146
+ }
2147
+ }
2148
+ } else {
2149
+ // Try arrow function style: service: async (ctx) => ({...})
2150
+ const arrowMatch = content.match(/service:\s*async\s*\([^)]*\)\s*=>\s*\(\{([\s\S]*?)\}\)/);
2151
+ if (arrowMatch) {
2152
+ const returnBlock = arrowMatch[1];
2153
+ const methodMatches = returnBlock.matchAll(/(\w+):\s*(?:async\s*)?\(/g);
2154
+ for (const m of methodMatches) {
2155
+ if (!methods.includes(m[1])) {
2156
+ methods.push(m[1]);
2157
+ }
2158
+ }
2159
+ }
2160
+ }
2161
+
2162
+ const depsMatch = content.match(/dependencies:\s*\[([^\]]*)\]/);
2163
+ const deps = depsMatch ? depsMatch[1].trim() : "";
2164
+
2165
+ const hasSchema = existsSync(join(pluginPath, "schema.ts"));
2166
+ const hasMigrations = existsSync(join(pluginPath, "migrations"));
2167
+
2168
+ plugins.push(`### ${entry}
2169
+ - **Methods:** ${methods.length > 0 ? methods.join(", ") : "none"}
2170
+ - **Dependencies:** ${deps || "none"}
2171
+ - **Database:** ${hasSchema || hasMigrations ? "yes" : "no"}
2172
+ - **Path:** ${projectConfig.pluginsDir}/${entry}/`);
2173
+ }
2174
+
2175
+ if (plugins.length === 0) {
2176
+ return "No plugins found. Use `create_plugin` to create one.";
2177
+ }
2178
+
2179
+ return `# Plugins (${plugins.length})\n\n${plugins.join("\n\n")}`;
2180
+ }
2181
+
2182
+ async function generateTypes(args: { target?: string }): Promise<string> {
2183
+ try {
2184
+ const proc = Bun.spawn(["bun", "run", "donkeylabs", "generate"], {
2185
+ cwd: projectRoot,
2186
+ stdout: "pipe",
2187
+ stderr: "pipe",
2188
+ });
2189
+
2190
+ const output = await new Response(proc.stdout).text();
2191
+ const error = await new Response(proc.stderr).text();
2192
+
2193
+ await proc.exited;
2194
+
2195
+ if (proc.exitCode !== 0) {
2196
+ return formatError(
2197
+ "Type generation failed",
2198
+ error || output,
2199
+ "Check that your project is properly configured and all plugins are valid."
2200
+ );
2201
+ }
2202
+
2203
+ return `## Types Generated Successfully
2204
+
2205
+ ${output}
2206
+
2207
+ Types are now up-to-date. Your IDE should recognize the new types.
2208
+ `;
2209
+ } catch (e) {
2210
+ return formatError(
2211
+ "Error running generation",
2212
+ String(e),
2213
+ "Make sure the donkeylabs CLI is installed: `bun add @donkeylabs/cli`"
2214
+ );
2215
+ }
2216
+ }
2217
+
2218
+ async function runCodegen(args: { command: string; outputPath?: string }): Promise<string> {
2219
+ const { command, outputPath } = args;
2220
+
2221
+ try {
2222
+ const cmdArgs = ["run", "donkeylabs", command];
2223
+ if (outputPath) {
2224
+ cmdArgs.push("--output", outputPath);
2225
+ }
2226
+
2227
+ const proc = Bun.spawn(["bun", ...cmdArgs], {
2228
+ cwd: projectRoot,
2229
+ stdout: "pipe",
2230
+ stderr: "pipe",
2231
+ });
2232
+
2233
+ const output = await new Response(proc.stdout).text();
2234
+ const error = await new Response(proc.stderr).text();
2235
+
2236
+ await proc.exited;
2237
+
2238
+ if (proc.exitCode !== 0) {
2239
+ return formatError(
2240
+ `Command 'donkeylabs ${command}' failed`,
2241
+ error || output,
2242
+ "Check the error message and fix any issues."
2243
+ );
2244
+ }
2245
+
2246
+ return `## Command Completed: donkeylabs ${command}
2247
+
2248
+ ${output}
2249
+ `;
2250
+ } catch (e) {
2251
+ return formatError(
2252
+ "Error running command",
2253
+ String(e),
2254
+ "Make sure the donkeylabs CLI is installed."
2255
+ );
2256
+ }
2257
+ }
2258
+
2259
+ async function generateClient(args: { outputPath?: string }): Promise<string> {
2260
+ const { outputPath } = args;
2261
+
2262
+ // Determine default output path based on project type
2263
+ const defaultOutput = projectConfig.adapter === "sveltekit"
2264
+ ? projectConfig.clientOutput || "src/lib/api.ts"
2265
+ : projectConfig.clientOutput || "./client/index.ts";
2266
+
2267
+ const finalOutput = outputPath || defaultOutput;
2268
+
2269
+ try {
2270
+ const cmdArgs = ["run", "donkeylabs", "generate"];
2271
+
2272
+ const proc = Bun.spawn(["bun", ...cmdArgs], {
2273
+ cwd: projectRoot,
2274
+ stdout: "pipe",
2275
+ stderr: "pipe",
2276
+ });
2277
+
2278
+ const output = await new Response(proc.stdout).text();
2279
+ const error = await new Response(proc.stderr).text();
2280
+
2281
+ await proc.exited;
2282
+
2283
+ if (proc.exitCode !== 0) {
2284
+ return formatError(
2285
+ "Client generation failed",
2286
+ error || output,
2287
+ "Check that your routes are properly defined and the project is configured."
2288
+ );
2289
+ }
2290
+
2291
+ // Build usage instructions based on project type
2292
+ let usage = "";
2293
+ if (projectConfig.adapter === "sveltekit") {
2294
+ const importPath = finalOutput.startsWith("src/lib/") ? "$lib/" + finalOutput.slice(8).replace(/\.ts$/, "") : finalOutput.replace(/\.ts$/, "");
2295
+ usage = `### SvelteKit Usage
2296
+
2297
+ **SSR (server-side, +page.server.ts):**
2298
+ \`\`\`typescript
2299
+ import { createApi } from '${importPath}';
2300
+
2301
+ export const load = async ({ locals }) => {
2302
+ const api = createApi({ locals }); // Direct calls, no HTTP!
2303
+ const data = await api.myRoute.get({});
2304
+ return { data };
2305
+ };
2306
+ \`\`\`
2307
+
2308
+ **Browser (+page.svelte):**
2309
+ \`\`\`svelte
2310
+ <script>
2311
+ import { createApi } from '${importPath}';
2312
+ const api = createApi(); // HTTP calls
2313
+ let data = $state(null);
2314
+ async function load() {
2315
+ data = await api.myRoute.get({});
2316
+ }
2317
+ </script>
2318
+ \`\`\`
2319
+
2320
+ **Key Benefit:** SSR calls go directly to your route handlers without HTTP overhead!
2321
+
2322
+ ### Using Route Types
2323
+
2324
+ Import types for forms, validation, or custom components:
2325
+ \`\`\`typescript
2326
+ import { type Routes } from '${importPath}';
2327
+
2328
+ // Use route types directly
2329
+ type UserInput = Routes.Users.Create.Input;
2330
+ type UserOutput = Routes.Users.Create.Output;
2331
+
2332
+ // Example: Form component
2333
+ function UserForm(props: { onSubmit: (data: UserInput) => void }) {
2334
+ // Fully typed input
2335
+ }
2336
+ \`\`\``;
2337
+ } else {
2338
+ usage = `### Usage
2339
+
2340
+ \`\`\`typescript
2341
+ import { createApiClient } from '${finalOutput.replace(/\.ts$/, "")}';
2342
+
2343
+ const api = createApiClient({ baseUrl: 'http://localhost:3000' });
2344
+
2345
+ // Typed route calls
2346
+ const result = await api.myRoute.get({ id: 1 });
2347
+
2348
+ // SSE events
2349
+ api.connect();
2350
+ api.on('events.new', (data) => console.log(data));
2351
+ \`\`\`
2352
+
2353
+ ### Using Route Types
2354
+
2355
+ Import types for your frontend code:
2356
+ \`\`\`typescript
2357
+ import { type Routes } from '${finalOutput.replace(/\.ts$/, "")}';
2358
+
2359
+ // Access route input/output types
2360
+ type UserInput = Routes.Users.Create.Input;
2361
+ type UserOutput = Routes.Users.Create.Output;
2362
+ \`\`\``;
2363
+ }
2364
+
2365
+ return `## API Client Generated
2366
+
2367
+ **Output:** ${finalOutput}
2368
+ **Type:** ${projectConfig.adapter === "sveltekit" ? "SvelteKit Unified Client (SSR + Browser)" : "Standard HTTP Client"}
2369
+
2370
+ ${usage}
2371
+
2372
+ ### When to Regenerate
2373
+ Run \`generate_client\` again when you:
2374
+ - Add new routes
2375
+ - Modify route input/output schemas
2376
+ - Add plugin events
2377
+
2378
+ ${output}
2379
+ `;
2380
+ } catch (e) {
2381
+ return formatError(
2382
+ "Error generating client",
2383
+ String(e),
2384
+ "Make sure the donkeylabs CLI is installed: `bun add @donkeylabs/cli`"
2385
+ );
2386
+ }
2387
+ }
2388
+
2389
+ // =============================================================================
2390
+ // SERVER SETUP
2391
+ // =============================================================================
2392
+
2393
+ const server = new Server(
2394
+ {
2395
+ name: "donkeylabs-mcp",
2396
+ version: "0.2.0",
2397
+ },
2398
+ {
2399
+ capabilities: {
2400
+ tools: {},
2401
+ resources: {},
2402
+ },
2403
+ }
2404
+ );
2405
+
2406
+ // Resource handlers
2407
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
2408
+ resources: RESOURCES.map(({ uri, name, description, mimeType }) => ({
2409
+ uri,
2410
+ name,
2411
+ description,
2412
+ mimeType,
2413
+ })),
2414
+ }));
2415
+
2416
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
2417
+ const { uri } = request.params;
2418
+ const content = await readResource(uri);
2419
+
2420
+ return {
2421
+ contents: [
2422
+ {
2423
+ uri,
2424
+ mimeType: "text/markdown",
2425
+ text: content,
2426
+ },
2427
+ ],
2428
+ };
2429
+ });
2430
+
2431
+ // Tool handlers
2432
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2433
+ tools,
2434
+ }));
2435
+
2436
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2437
+ const { name, arguments: args } = request.params;
2438
+
2439
+ try {
2440
+ let result: string;
2441
+
2442
+ switch (name) {
2443
+ case "get_project_info":
2444
+ result = await getProjectInfo();
2445
+ break;
2446
+ case "get_architecture_guidance":
2447
+ result = await getArchitectureGuidance(args as { task: string });
2448
+ break;
2449
+ case "create_plugin":
2450
+ result = await createPlugin(args as Parameters<typeof createPlugin>[0]);
2451
+ break;
2452
+ case "add_service_method":
2453
+ result = await addServiceMethod(args as Parameters<typeof addServiceMethod>[0]);
2454
+ break;
2455
+ case "add_migration":
2456
+ result = await addMigration(args as Parameters<typeof addMigration>[0]);
2457
+ break;
2458
+ case "create_router":
2459
+ result = await createRouter(args as Parameters<typeof createRouter>[0]);
2460
+ break;
2461
+ case "add_route":
2462
+ result = await addRoute(args as Parameters<typeof addRoute>[0]);
2463
+ break;
2464
+ case "add_handler_to_router":
2465
+ result = await addHandlerToRouter(args as Parameters<typeof addHandlerToRouter>[0]);
2466
+ break;
2467
+ case "extend_plugin":
2468
+ result = await extendPlugin(args as Parameters<typeof extendPlugin>[0]);
2469
+ break;
2470
+ case "add_cron":
2471
+ result = await addCron(args as Parameters<typeof addCron>[0]);
2472
+ break;
2473
+ case "add_event":
2474
+ result = await addEvent(args as Parameters<typeof addEvent>[0]);
2475
+ break;
2476
+ case "add_async_job":
2477
+ result = await addAsyncJob(args as Parameters<typeof addAsyncJob>[0]);
2478
+ break;
2479
+ case "add_sse_route":
2480
+ result = await addSSERoute(args as Parameters<typeof addSSERoute>[0]);
2481
+ break;
2482
+ case "list_plugins":
2483
+ result = await listPlugins();
2484
+ break;
2485
+ case "generate_types":
2486
+ result = await generateTypes(args as { target?: string });
2487
+ break;
2488
+ case "run_codegen":
2489
+ result = await runCodegen(args as Parameters<typeof runCodegen>[0]);
2490
+ break;
2491
+ case "generate_client":
2492
+ result = await generateClient(args as { outputPath?: string });
2493
+ break;
2494
+ default:
2495
+ result = formatError(
2496
+ `Unknown tool: ${name}`,
2497
+ undefined,
2498
+ `Available tools: ${tools.map(t => t.name).join(", ")}`
2499
+ );
2500
+ }
2501
+
2502
+ return {
2503
+ content: [{ type: "text", text: result }],
2504
+ };
2505
+ } catch (error) {
2506
+ return {
2507
+ content: [
2508
+ {
2509
+ type: "text",
2510
+ text: formatError(
2511
+ error instanceof Error ? error.message : String(error),
2512
+ undefined,
2513
+ "This may be a bug. Please report it."
2514
+ ),
2515
+ },
2516
+ ],
2517
+ isError: true,
2518
+ };
2519
+ }
2520
+ });
2521
+
2522
+ // Start server
2523
+ const transport = new StdioServerTransport();
2524
+ await server.connect(transport);