@atlashub/smartstack-mcp 1.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.
package/dist/index.js ADDED
@@ -0,0 +1,2849 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+
4
+ // src/server.ts
5
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import {
8
+ CallToolRequestSchema,
9
+ ListToolsRequestSchema,
10
+ ListResourcesRequestSchema,
11
+ ReadResourceRequestSchema
12
+ } from "@modelcontextprotocol/sdk/types.js";
13
+
14
+ // src/lib/logger.ts
15
+ var Logger = class {
16
+ level = "info";
17
+ levels = {
18
+ debug: 0,
19
+ info: 1,
20
+ warn: 2,
21
+ error: 3
22
+ };
23
+ setLevel(level) {
24
+ this.level = level;
25
+ }
26
+ shouldLog(level) {
27
+ return this.levels[level] >= this.levels[this.level];
28
+ }
29
+ formatEntry(level, message, data) {
30
+ const entry = {
31
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
32
+ level,
33
+ message,
34
+ ...data !== void 0 && { data }
35
+ };
36
+ return JSON.stringify(entry);
37
+ }
38
+ debug(message, data) {
39
+ if (this.shouldLog("debug")) {
40
+ console.error(this.formatEntry("debug", message, data));
41
+ }
42
+ }
43
+ info(message, data) {
44
+ if (this.shouldLog("info")) {
45
+ console.error(this.formatEntry("info", message, data));
46
+ }
47
+ }
48
+ warn(message, data) {
49
+ if (this.shouldLog("warn")) {
50
+ console.error(this.formatEntry("warn", message, data));
51
+ }
52
+ }
53
+ error(message, data) {
54
+ if (this.shouldLog("error")) {
55
+ console.error(this.formatEntry("error", message, data));
56
+ }
57
+ }
58
+ // Tool execution logging
59
+ toolStart(toolName, args) {
60
+ this.info(`Tool started: ${toolName}`, { args });
61
+ }
62
+ toolEnd(toolName, success, duration) {
63
+ this.info(`Tool completed: ${toolName}`, { success, duration });
64
+ }
65
+ toolError(toolName, error) {
66
+ this.error(`Tool failed: ${toolName}`, {
67
+ error: error.message,
68
+ stack: error.stack
69
+ });
70
+ }
71
+ };
72
+ var logger = new Logger();
73
+ var envLevel = process.env.LOG_LEVEL;
74
+ if (envLevel && ["debug", "info", "warn", "error"].includes(envLevel)) {
75
+ logger.setLevel(envLevel);
76
+ }
77
+
78
+ // src/config.ts
79
+ import path2 from "path";
80
+
81
+ // src/utils/fs.ts
82
+ import fs from "fs-extra";
83
+ import path from "path";
84
+ import { glob } from "glob";
85
+ async function fileExists(filePath) {
86
+ try {
87
+ const stat = await fs.stat(filePath);
88
+ return stat.isFile();
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+ async function directoryExists(dirPath) {
94
+ try {
95
+ const stat = await fs.stat(dirPath);
96
+ return stat.isDirectory();
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+ async function ensureDirectory(dirPath) {
102
+ await fs.ensureDir(dirPath);
103
+ }
104
+ async function readJson(filePath) {
105
+ const content = await fs.readFile(filePath, "utf-8");
106
+ return JSON.parse(content);
107
+ }
108
+ async function readText(filePath) {
109
+ return fs.readFile(filePath, "utf-8");
110
+ }
111
+ async function writeText(filePath, content) {
112
+ await fs.ensureDir(path.dirname(filePath));
113
+ await fs.writeFile(filePath, content, "utf-8");
114
+ }
115
+ async function findFiles(pattern, options = {}) {
116
+ const { cwd = process.cwd(), ignore = [] } = options;
117
+ const files = await glob(pattern, {
118
+ cwd,
119
+ ignore: ["**/node_modules/**", "**/bin/**", "**/obj/**", ...ignore],
120
+ absolute: true,
121
+ nodir: true
122
+ });
123
+ return files;
124
+ }
125
+
126
+ // src/config.ts
127
+ var defaultConfig = {
128
+ version: "1.0.0",
129
+ smartstack: {
130
+ projectPath: process.env.SMARTSTACK_PROJECT_PATH || "D:/SmartStack.app/02-Develop",
131
+ apiUrl: process.env.SMARTSTACK_API_URL || "https://localhost:5001",
132
+ apiEnabled: process.env.SMARTSTACK_API_ENABLED !== "false"
133
+ },
134
+ conventions: {
135
+ tablePrefix: {
136
+ core: "Core_",
137
+ client: "Client_"
138
+ },
139
+ migrationFormat: "YYYYMMDD_{Prefix}_NNN_{Description}",
140
+ namespaces: {
141
+ domain: "SmartStack.Domain",
142
+ application: "SmartStack.Application",
143
+ infrastructure: "SmartStack.Infrastructure",
144
+ api: "SmartStack.Api"
145
+ },
146
+ servicePattern: {
147
+ interface: "I{Name}Service",
148
+ implementation: "{Name}Service"
149
+ }
150
+ },
151
+ efcore: {
152
+ contexts: [
153
+ {
154
+ name: "ApplicationDbContext",
155
+ projectPath: "auto-detect",
156
+ migrationsFolder: "Migrations"
157
+ }
158
+ ],
159
+ validation: {
160
+ checkModelSnapshot: true,
161
+ checkMigrationOrder: true,
162
+ requireBuildSuccess: true
163
+ }
164
+ },
165
+ scaffolding: {
166
+ outputPath: "auto-detect",
167
+ templates: {
168
+ service: "templates/service-extension.cs.hbs",
169
+ entity: "templates/entity-extension.cs.hbs",
170
+ controller: "templates/controller.cs.hbs",
171
+ component: "templates/component.tsx.hbs"
172
+ }
173
+ }
174
+ };
175
+ var cachedConfig = null;
176
+ async function getConfig() {
177
+ if (cachedConfig) {
178
+ return cachedConfig;
179
+ }
180
+ const configPath = path2.join(process.cwd(), "config", "default-config.json");
181
+ if (await fileExists(configPath)) {
182
+ try {
183
+ const fileConfig = await readJson(configPath);
184
+ cachedConfig = mergeConfig(defaultConfig, fileConfig);
185
+ logger.info("Configuration loaded from file", { path: configPath });
186
+ } catch (error) {
187
+ logger.warn("Failed to load config file, using defaults", { error });
188
+ cachedConfig = defaultConfig;
189
+ }
190
+ } else {
191
+ logger.debug("No config file found, using defaults");
192
+ cachedConfig = defaultConfig;
193
+ }
194
+ if (process.env.SMARTSTACK_PROJECT_PATH) {
195
+ cachedConfig.smartstack.projectPath = process.env.SMARTSTACK_PROJECT_PATH;
196
+ }
197
+ if (process.env.SMARTSTACK_API_URL) {
198
+ cachedConfig.smartstack.apiUrl = process.env.SMARTSTACK_API_URL;
199
+ }
200
+ return cachedConfig;
201
+ }
202
+ function mergeConfig(base, override) {
203
+ return {
204
+ ...base,
205
+ ...override,
206
+ smartstack: {
207
+ ...base.smartstack,
208
+ ...override.smartstack
209
+ },
210
+ conventions: {
211
+ ...base.conventions,
212
+ ...override.conventions,
213
+ tablePrefix: {
214
+ ...base.conventions.tablePrefix,
215
+ ...override.conventions?.tablePrefix
216
+ },
217
+ namespaces: {
218
+ ...base.conventions.namespaces,
219
+ ...override.conventions?.namespaces
220
+ },
221
+ servicePattern: {
222
+ ...base.conventions.servicePattern,
223
+ ...override.conventions?.servicePattern
224
+ }
225
+ },
226
+ efcore: {
227
+ ...base.efcore,
228
+ ...override.efcore,
229
+ contexts: override.efcore?.contexts || base.efcore.contexts,
230
+ validation: {
231
+ ...base.efcore.validation,
232
+ ...override.efcore?.validation
233
+ }
234
+ },
235
+ scaffolding: {
236
+ ...base.scaffolding,
237
+ ...override.scaffolding,
238
+ templates: {
239
+ ...base.scaffolding.templates,
240
+ ...override.scaffolding?.templates
241
+ }
242
+ }
243
+ };
244
+ }
245
+
246
+ // src/types/index.ts
247
+ import { z } from "zod";
248
+ var SmartStackConfigSchema = z.object({
249
+ projectPath: z.string(),
250
+ apiUrl: z.string().url().optional(),
251
+ apiEnabled: z.boolean().default(true)
252
+ });
253
+ var ConventionsConfigSchema = z.object({
254
+ schemas: z.object({
255
+ platform: z.string().default("core"),
256
+ extensions: z.string().default("extensions")
257
+ }),
258
+ tablePrefixes: z.array(z.string()).default([
259
+ "auth_",
260
+ "nav_",
261
+ "usr_",
262
+ "ai_",
263
+ "cfg_",
264
+ "wkf_",
265
+ "support_",
266
+ "entra_",
267
+ "ref_",
268
+ "loc_",
269
+ "lic_"
270
+ ]),
271
+ migrationFormat: z.string().default("YYYYMMDD_NNN_{Description}"),
272
+ namespaces: z.object({
273
+ domain: z.string(),
274
+ application: z.string(),
275
+ infrastructure: z.string(),
276
+ api: z.string()
277
+ }),
278
+ servicePattern: z.object({
279
+ interface: z.string().default("I{Name}Service"),
280
+ implementation: z.string().default("{Name}Service")
281
+ })
282
+ });
283
+ var EfCoreContextSchema = z.object({
284
+ name: z.string(),
285
+ projectPath: z.string(),
286
+ migrationsFolder: z.string().default("Migrations")
287
+ });
288
+ var EfCoreConfigSchema = z.object({
289
+ contexts: z.array(EfCoreContextSchema),
290
+ validation: z.object({
291
+ checkModelSnapshot: z.boolean().default(true),
292
+ checkMigrationOrder: z.boolean().default(true),
293
+ requireBuildSuccess: z.boolean().default(true)
294
+ })
295
+ });
296
+ var ScaffoldingConfigSchema = z.object({
297
+ outputPath: z.string(),
298
+ templates: z.object({
299
+ service: z.string(),
300
+ entity: z.string(),
301
+ controller: z.string(),
302
+ component: z.string()
303
+ })
304
+ });
305
+ var ConfigSchema = z.object({
306
+ version: z.string(),
307
+ smartstack: SmartStackConfigSchema,
308
+ conventions: ConventionsConfigSchema,
309
+ efcore: EfCoreConfigSchema,
310
+ scaffolding: ScaffoldingConfigSchema
311
+ });
312
+ var ValidateConventionsInputSchema = z.object({
313
+ path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
314
+ checks: z.array(z.enum(["tables", "migrations", "services", "namespaces", "all"])).default(["all"]).describe("Types of checks to perform")
315
+ });
316
+ var CheckMigrationsInputSchema = z.object({
317
+ projectPath: z.string().optional().describe("EF Core project path"),
318
+ branch: z.string().optional().describe("Git branch to check (default: current)"),
319
+ compareBranch: z.string().optional().describe("Branch to compare against")
320
+ });
321
+ var ScaffoldExtensionInputSchema = z.object({
322
+ type: z.enum(["service", "entity", "controller", "component"]).describe("Type of extension to scaffold"),
323
+ name: z.string().describe('Name of the extension (e.g., "UserProfile", "Order")'),
324
+ options: z.object({
325
+ namespace: z.string().optional().describe("Custom namespace"),
326
+ baseEntity: z.string().optional().describe("Base entity to extend (for entity type)"),
327
+ methods: z.array(z.string()).optional().describe("Methods to generate (for service type)"),
328
+ outputPath: z.string().optional().describe("Custom output path")
329
+ }).optional()
330
+ });
331
+ var ApiDocsInputSchema = z.object({
332
+ endpoint: z.string().optional().describe('Filter by endpoint path (e.g., "/api/users")'),
333
+ format: z.enum(["markdown", "json", "openapi"]).default("markdown").describe("Output format"),
334
+ controller: z.string().optional().describe("Filter by controller name")
335
+ });
336
+
337
+ // src/lib/detector.ts
338
+ import path4 from "path";
339
+
340
+ // src/utils/git.ts
341
+ import { exec } from "child_process";
342
+ import { promisify } from "util";
343
+ import path3 from "path";
344
+ var execAsync = promisify(exec);
345
+ async function git(command, cwd) {
346
+ const options = cwd ? { cwd } : {};
347
+ const { stdout } = await execAsync(`git ${command}`, options);
348
+ return stdout.trim();
349
+ }
350
+ async function isGitRepo(cwd) {
351
+ const gitDir = path3.join(cwd || process.cwd(), ".git");
352
+ return directoryExists(gitDir);
353
+ }
354
+ async function getCurrentBranch(cwd) {
355
+ try {
356
+ return await git("branch --show-current", cwd);
357
+ } catch {
358
+ return null;
359
+ }
360
+ }
361
+ async function branchExists(branch, cwd) {
362
+ try {
363
+ await git(`rev-parse --verify ${branch}`, cwd);
364
+ return true;
365
+ } catch {
366
+ return false;
367
+ }
368
+ }
369
+ async function getFileFromBranch(branch, filePath, cwd) {
370
+ try {
371
+ return await git(`show ${branch}:${filePath}`, cwd);
372
+ } catch {
373
+ return null;
374
+ }
375
+ }
376
+ async function getDiff(fromBranch, toBranch, filePath, cwd) {
377
+ try {
378
+ const pathArg = filePath ? ` -- ${filePath}` : "";
379
+ return await git(`diff ${fromBranch}...${toBranch}${pathArg}`, cwd);
380
+ } catch {
381
+ return "";
382
+ }
383
+ }
384
+
385
+ // src/utils/dotnet.ts
386
+ import { exec as exec2 } from "child_process";
387
+ import { promisify as promisify2 } from "util";
388
+ var execAsync2 = promisify2(exec2);
389
+ async function findCsprojFiles(cwd) {
390
+ return findFiles("**/*.csproj", { cwd: cwd || process.cwd() });
391
+ }
392
+ async function hasEfCore(csprojPath) {
393
+ try {
394
+ const content = await readText(csprojPath);
395
+ return content.includes("Microsoft.EntityFrameworkCore");
396
+ } catch {
397
+ return false;
398
+ }
399
+ }
400
+ async function findDbContextName(projectPath) {
401
+ try {
402
+ const csFiles = await findFiles("**/*.cs", { cwd: projectPath });
403
+ for (const file of csFiles) {
404
+ const content = await readText(file);
405
+ const match = content.match(/class\s+(\w+)\s*:\s*(?:\w+,\s*)*DbContext/);
406
+ if (match) {
407
+ return match[1];
408
+ }
409
+ }
410
+ return null;
411
+ } catch {
412
+ return null;
413
+ }
414
+ }
415
+ async function getTargetFramework(csprojPath) {
416
+ try {
417
+ const content = await readText(csprojPath);
418
+ const match = content.match(/<TargetFramework>([^<]+)<\/TargetFramework>/);
419
+ return match ? match[1] : null;
420
+ } catch {
421
+ return null;
422
+ }
423
+ }
424
+
425
+ // src/lib/detector.ts
426
+ async function detectProject(projectPath) {
427
+ logger.debug("Detecting project info", { path: projectPath });
428
+ const info = {
429
+ name: path4.basename(projectPath),
430
+ version: "0.0.0",
431
+ isGitRepo: false,
432
+ hasDotNet: false,
433
+ hasEfCore: false,
434
+ hasReact: false,
435
+ csprojFiles: []
436
+ };
437
+ if (!await directoryExists(projectPath)) {
438
+ logger.warn("Project path does not exist", { path: projectPath });
439
+ return info;
440
+ }
441
+ info.isGitRepo = await isGitRepo(projectPath);
442
+ if (info.isGitRepo) {
443
+ info.currentBranch = await getCurrentBranch(projectPath) || void 0;
444
+ }
445
+ info.csprojFiles = await findCsprojFiles(projectPath);
446
+ info.hasDotNet = info.csprojFiles.length > 0;
447
+ if (info.hasDotNet) {
448
+ for (const csproj of info.csprojFiles) {
449
+ if (await hasEfCore(csproj)) {
450
+ info.hasEfCore = true;
451
+ info.dbContextName = await findDbContextName(path4.dirname(csproj)) || void 0;
452
+ break;
453
+ }
454
+ }
455
+ }
456
+ const packageJsonPath = path4.join(projectPath, "web", "smartstack-web", "package.json");
457
+ if (await fileExists(packageJsonPath)) {
458
+ try {
459
+ const packageJson = JSON.parse(await readText(packageJsonPath));
460
+ info.hasReact = !!packageJson.dependencies?.react;
461
+ info.version = packageJson.version || info.version;
462
+ } catch {
463
+ }
464
+ }
465
+ if (info.csprojFiles.length > 0) {
466
+ const mainCsproj = info.csprojFiles.find((f) => f.includes("SmartStack.Api")) || info.csprojFiles[0];
467
+ const targetFramework = await getTargetFramework(mainCsproj);
468
+ if (targetFramework) {
469
+ logger.debug("Target framework detected", { framework: targetFramework });
470
+ }
471
+ }
472
+ logger.info("Project detected", info);
473
+ return info;
474
+ }
475
+ async function findSmartStackStructure(projectPath) {
476
+ const structure = { root: projectPath };
477
+ const csprojFiles = await findCsprojFiles(projectPath);
478
+ for (const csproj of csprojFiles) {
479
+ const projectName = path4.basename(csproj, ".csproj").toLowerCase();
480
+ const projectDir = path4.dirname(csproj);
481
+ if (projectName.includes("domain")) {
482
+ structure.domain = projectDir;
483
+ } else if (projectName.includes("application")) {
484
+ structure.application = projectDir;
485
+ } else if (projectName.includes("infrastructure")) {
486
+ structure.infrastructure = projectDir;
487
+ } else if (projectName.includes("api")) {
488
+ structure.api = projectDir;
489
+ }
490
+ }
491
+ if (structure.infrastructure) {
492
+ const migrationsPath = path4.join(structure.infrastructure, "Persistence", "Migrations");
493
+ if (await directoryExists(migrationsPath)) {
494
+ structure.migrations = migrationsPath;
495
+ }
496
+ }
497
+ const webPath = path4.join(projectPath, "web", "smartstack-web");
498
+ if (await directoryExists(webPath)) {
499
+ structure.web = webPath;
500
+ }
501
+ return structure;
502
+ }
503
+ async function findEntityFiles(domainPath) {
504
+ const entityFiles = await findFiles("**/*.cs", { cwd: domainPath });
505
+ const entities = [];
506
+ for (const file of entityFiles) {
507
+ const content = await readText(file);
508
+ if (content.match(/public\s+(?:class|record)\s+\w+/) && content.match(/public\s+(?:int|long|Guid|string)\s+Id\s*\{/)) {
509
+ entities.push(file);
510
+ }
511
+ }
512
+ return entities;
513
+ }
514
+ async function findControllerFiles(apiPath) {
515
+ const files = await findFiles("**/*Controller.cs", { cwd: apiPath });
516
+ return files;
517
+ }
518
+
519
+ // src/tools/validate-conventions.ts
520
+ import path5 from "path";
521
+ var validateConventionsTool = {
522
+ name: "validate_conventions",
523
+ description: "Validate AtlasHub/SmartStack conventions: SQL schemas (core/extensions), domain table prefixes (auth_, nav_, ai_, etc.), migration naming (YYYYMMDD_NNN_*), service interfaces (I*Service), namespace structure",
524
+ inputSchema: {
525
+ type: "object",
526
+ properties: {
527
+ path: {
528
+ type: "string",
529
+ description: "Project path to validate (default: SmartStack.app path from config)"
530
+ },
531
+ checks: {
532
+ type: "array",
533
+ items: {
534
+ type: "string",
535
+ enum: ["tables", "migrations", "services", "namespaces", "all"]
536
+ },
537
+ description: "Types of checks to perform",
538
+ default: ["all"]
539
+ }
540
+ }
541
+ }
542
+ };
543
+ async function handleValidateConventions(args, config) {
544
+ const input = ValidateConventionsInputSchema.parse(args);
545
+ const projectPath = input.path || config.smartstack.projectPath;
546
+ const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces"] : input.checks;
547
+ logger.info("Validating conventions", { projectPath, checks });
548
+ const result = {
549
+ valid: true,
550
+ errors: [],
551
+ warnings: [],
552
+ summary: ""
553
+ };
554
+ const structure = await findSmartStackStructure(projectPath);
555
+ if (checks.includes("tables")) {
556
+ await validateTablePrefixes(structure, config, result);
557
+ }
558
+ if (checks.includes("migrations")) {
559
+ await validateMigrationNaming(structure, config, result);
560
+ }
561
+ if (checks.includes("services")) {
562
+ await validateServiceInterfaces(structure, config, result);
563
+ }
564
+ if (checks.includes("namespaces")) {
565
+ await validateNamespaces(structure, config, result);
566
+ }
567
+ result.valid = result.errors.length === 0;
568
+ result.summary = generateSummary(result, checks);
569
+ return formatResult(result);
570
+ }
571
+ async function validateTablePrefixes(structure, config, result) {
572
+ if (!structure.infrastructure) {
573
+ result.warnings.push({
574
+ type: "warning",
575
+ category: "tables",
576
+ message: "Infrastructure project not found, skipping table validation"
577
+ });
578
+ return;
579
+ }
580
+ const configFiles = await findFiles("**/Configurations/**/*.cs", {
581
+ cwd: structure.infrastructure
582
+ });
583
+ const validSchemas = [config.conventions.schemas.platform, config.conventions.schemas.extensions];
584
+ const validPrefixes = config.conventions.tablePrefixes;
585
+ const schemaConstantsMap = {
586
+ "SchemaConstants.Core": config.conventions.schemas.platform,
587
+ "SchemaConstants.Extensions": config.conventions.schemas.extensions
588
+ };
589
+ for (const file of configFiles) {
590
+ const content = await readText(file);
591
+ const tableWithStringSchemaMatches = content.matchAll(/\.ToTable\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g);
592
+ for (const match of tableWithStringSchemaMatches) {
593
+ const tableName = match[1];
594
+ const schemaName = match[2];
595
+ if (!validSchemas.includes(schemaName)) {
596
+ result.errors.push({
597
+ type: "error",
598
+ category: "tables",
599
+ message: `Table "${tableName}" uses invalid schema "${schemaName}"`,
600
+ file: path5.relative(structure.root, file),
601
+ suggestion: `Use schema "${config.conventions.schemas.platform}" for SmartStack tables or "${config.conventions.schemas.extensions}" for client extensions`
602
+ });
603
+ }
604
+ const hasValidPrefix = validPrefixes.some((prefix) => tableName.startsWith(prefix));
605
+ if (!hasValidPrefix && !tableName.startsWith("__")) {
606
+ result.warnings.push({
607
+ type: "warning",
608
+ category: "tables",
609
+ message: `Table "${tableName}" does not use a standard domain prefix`,
610
+ file: path5.relative(structure.root, file),
611
+ suggestion: `Consider using a domain prefix: ${validPrefixes.slice(0, 5).join(", ")}, etc.`
612
+ });
613
+ }
614
+ }
615
+ const tableWithConstantSchemaMatches = content.matchAll(/\.ToTable\s*\(\s*"([^"]+)"\s*,\s*(SchemaConstants\.\w+)\s*\)/g);
616
+ for (const match of tableWithConstantSchemaMatches) {
617
+ const tableName = match[1];
618
+ const schemaConstant = match[2];
619
+ const resolvedSchema = schemaConstantsMap[schemaConstant];
620
+ if (!resolvedSchema) {
621
+ result.errors.push({
622
+ type: "error",
623
+ category: "tables",
624
+ message: `Table "${tableName}" uses unknown schema constant "${schemaConstant}"`,
625
+ file: path5.relative(structure.root, file),
626
+ suggestion: `Use SchemaConstants.Core for SmartStack tables or SchemaConstants.Extensions for client extensions`
627
+ });
628
+ }
629
+ const hasValidPrefix = validPrefixes.some((prefix) => tableName.startsWith(prefix));
630
+ if (!hasValidPrefix && !tableName.startsWith("__")) {
631
+ result.warnings.push({
632
+ type: "warning",
633
+ category: "tables",
634
+ message: `Table "${tableName}" does not use a standard domain prefix`,
635
+ file: path5.relative(structure.root, file),
636
+ suggestion: `Consider using a domain prefix: ${validPrefixes.slice(0, 5).join(", ")}, etc.`
637
+ });
638
+ }
639
+ }
640
+ const tableWithoutSchemaMatches = content.matchAll(/\.ToTable\s*\(\s*"([^"]+)"\s*\)(?!\s*,)/g);
641
+ for (const match of tableWithoutSchemaMatches) {
642
+ const tableName = match[1];
643
+ if (!tableName.startsWith("__")) {
644
+ result.errors.push({
645
+ type: "error",
646
+ category: "tables",
647
+ message: `Table "${tableName}" is missing schema specification`,
648
+ file: path5.relative(structure.root, file),
649
+ suggestion: `Add schema: .ToTable("${tableName}", SchemaConstants.Core)`
650
+ });
651
+ }
652
+ }
653
+ }
654
+ }
655
+ async function validateMigrationNaming(structure, _config, result) {
656
+ if (!structure.migrations) {
657
+ result.warnings.push({
658
+ type: "warning",
659
+ category: "migrations",
660
+ message: "Migrations folder not found, skipping migration validation"
661
+ });
662
+ return;
663
+ }
664
+ const migrationFiles = await findFiles("*.cs", { cwd: structure.migrations });
665
+ const migrationPattern = /^(\d{8})_(\d{3})_(.+)\.cs$/;
666
+ const designerPattern = /\.Designer\.cs$/;
667
+ for (const file of migrationFiles) {
668
+ const fileName = path5.basename(file);
669
+ if (designerPattern.test(fileName) || fileName.includes("ModelSnapshot")) {
670
+ continue;
671
+ }
672
+ if (!migrationPattern.test(fileName)) {
673
+ result.errors.push({
674
+ type: "error",
675
+ category: "migrations",
676
+ message: `Migration "${fileName}" does not follow naming convention`,
677
+ file: path5.relative(structure.root, file),
678
+ suggestion: `Expected format: YYYYMMDD_NNN_Description.cs (e.g., 20260115_001_InitialCreate.cs)`
679
+ });
680
+ }
681
+ }
682
+ const orderedMigrations = migrationFiles.map((f) => path5.basename(f)).filter((f) => migrationPattern.test(f) && !f.includes("Designer")).sort();
683
+ for (let i = 1; i < orderedMigrations.length; i++) {
684
+ const prev = orderedMigrations[i - 1];
685
+ const curr = orderedMigrations[i];
686
+ const prevDate = prev.substring(0, 8);
687
+ const currDate = curr.substring(0, 8);
688
+ if (currDate < prevDate) {
689
+ result.warnings.push({
690
+ type: "warning",
691
+ category: "migrations",
692
+ message: `Migration order issue: "${curr}" dated before "${prev}"`
693
+ });
694
+ }
695
+ }
696
+ }
697
+ async function validateServiceInterfaces(structure, config, result) {
698
+ if (!structure.application) {
699
+ result.warnings.push({
700
+ type: "warning",
701
+ category: "services",
702
+ message: "Application project not found, skipping service validation"
703
+ });
704
+ return;
705
+ }
706
+ const serviceFiles = await findFiles("**/*Service.cs", {
707
+ cwd: structure.application
708
+ });
709
+ for (const file of serviceFiles) {
710
+ const content = await readText(file);
711
+ const fileName = path5.basename(file, ".cs");
712
+ if (fileName.startsWith("I")) continue;
713
+ const expectedInterface = `I${fileName}`;
714
+ const interfacePattern = new RegExp(`:\\s*${expectedInterface}\\b`);
715
+ if (!interfacePattern.test(content)) {
716
+ const declaresInterface = content.includes(`interface ${expectedInterface}`);
717
+ if (!declaresInterface) {
718
+ result.warnings.push({
719
+ type: "warning",
720
+ category: "services",
721
+ message: `Service "${fileName}" should implement "${expectedInterface}"`,
722
+ file: path5.relative(structure.root, file),
723
+ suggestion: `Create interface ${expectedInterface} and implement it`
724
+ });
725
+ }
726
+ }
727
+ }
728
+ }
729
+ async function validateNamespaces(structure, config, result) {
730
+ const layers = [
731
+ { path: structure.domain, expected: config.conventions.namespaces.domain, name: "Domain" },
732
+ { path: structure.application, expected: config.conventions.namespaces.application, name: "Application" },
733
+ { path: structure.infrastructure, expected: config.conventions.namespaces.infrastructure, name: "Infrastructure" },
734
+ { path: structure.api, expected: config.conventions.namespaces.api, name: "Api" }
735
+ ];
736
+ for (const layer of layers) {
737
+ if (!layer.path) continue;
738
+ const csFiles = await findFiles("**/*.cs", { cwd: layer.path });
739
+ for (const file of csFiles.slice(0, 10)) {
740
+ const content = await readText(file);
741
+ const namespaceMatch = content.match(/namespace\s+([\w.]+)/);
742
+ if (namespaceMatch) {
743
+ const namespace = namespaceMatch[1];
744
+ if (!namespace.startsWith(layer.expected)) {
745
+ result.errors.push({
746
+ type: "error",
747
+ category: "namespaces",
748
+ message: `${layer.name} file has incorrect namespace "${namespace}"`,
749
+ file: path5.relative(structure.root, file),
750
+ suggestion: `Should start with "${layer.expected}"`
751
+ });
752
+ }
753
+ }
754
+ }
755
+ }
756
+ }
757
+ function generateSummary(result, checks) {
758
+ const parts = [];
759
+ parts.push(`Checks performed: ${checks.join(", ")}`);
760
+ parts.push(`Errors: ${result.errors.length}`);
761
+ parts.push(`Warnings: ${result.warnings.length}`);
762
+ parts.push(`Status: ${result.valid ? "PASSED" : "FAILED"}`);
763
+ return parts.join(" | ");
764
+ }
765
+ function formatResult(result) {
766
+ const lines = [];
767
+ lines.push("# Conventions Validation Report");
768
+ lines.push("");
769
+ lines.push(`## Summary`);
770
+ lines.push(`- **Status**: ${result.valid ? "\u2705 PASSED" : "\u274C FAILED"}`);
771
+ lines.push(`- **Errors**: ${result.errors.length}`);
772
+ lines.push(`- **Warnings**: ${result.warnings.length}`);
773
+ lines.push("");
774
+ if (result.errors.length > 0) {
775
+ lines.push("## Errors");
776
+ lines.push("");
777
+ for (const error of result.errors) {
778
+ lines.push(`### \u274C ${error.category}: ${error.message}`);
779
+ if (error.file) lines.push(`- **File**: \`${error.file}\``);
780
+ if (error.line) lines.push(`- **Line**: ${error.line}`);
781
+ if (error.suggestion) lines.push(`- **Suggestion**: ${error.suggestion}`);
782
+ lines.push("");
783
+ }
784
+ }
785
+ if (result.warnings.length > 0) {
786
+ lines.push("## Warnings");
787
+ lines.push("");
788
+ for (const warning of result.warnings) {
789
+ lines.push(`### \u26A0\uFE0F ${warning.category}: ${warning.message}`);
790
+ if (warning.file) lines.push(`- **File**: \`${warning.file}\``);
791
+ if (warning.suggestion) lines.push(`- **Suggestion**: ${warning.suggestion}`);
792
+ lines.push("");
793
+ }
794
+ }
795
+ if (result.errors.length === 0 && result.warnings.length === 0) {
796
+ lines.push("## Result");
797
+ lines.push("");
798
+ lines.push("All conventions validated successfully! \u2728");
799
+ }
800
+ return lines.join("\n");
801
+ }
802
+
803
+ // src/tools/check-migrations.ts
804
+ import path6 from "path";
805
+ var checkMigrationsTool = {
806
+ name: "check_migrations",
807
+ description: "Analyze EF Core migrations for conflicts, ordering issues, and ModelSnapshot discrepancies between branches",
808
+ inputSchema: {
809
+ type: "object",
810
+ properties: {
811
+ projectPath: {
812
+ type: "string",
813
+ description: "EF Core project path (default: auto-detect from config)"
814
+ },
815
+ branch: {
816
+ type: "string",
817
+ description: "Git branch to check (default: current branch)"
818
+ },
819
+ compareBranch: {
820
+ type: "string",
821
+ description: 'Branch to compare against (e.g., "develop")'
822
+ }
823
+ }
824
+ }
825
+ };
826
+ async function handleCheckMigrations(args, config) {
827
+ const input = CheckMigrationsInputSchema.parse(args);
828
+ const projectPath = input.projectPath || config.smartstack.projectPath;
829
+ logger.info("Checking migrations", { projectPath, branch: input.branch });
830
+ const structure = await findSmartStackStructure(projectPath);
831
+ if (!structure.migrations) {
832
+ return "# Migration Check\n\n\u274C No migrations folder found in the project.";
833
+ }
834
+ const result = {
835
+ hasConflicts: false,
836
+ migrations: [],
837
+ conflicts: [],
838
+ suggestions: []
839
+ };
840
+ const currentBranch = input.branch || await getCurrentBranch(projectPath) || "unknown";
841
+ result.migrations = await parseMigrations(structure.migrations, structure.root);
842
+ checkNamingConventions(result, config);
843
+ checkChronologicalOrder(result);
844
+ if (input.compareBranch && await branchExists(input.compareBranch, projectPath)) {
845
+ await checkBranchConflicts(
846
+ result,
847
+ structure,
848
+ currentBranch,
849
+ input.compareBranch,
850
+ projectPath
851
+ );
852
+ }
853
+ await checkModelSnapshot(result, structure);
854
+ result.hasConflicts = result.conflicts.length > 0;
855
+ generateSuggestions(result);
856
+ return formatResult2(result, currentBranch, input.compareBranch);
857
+ }
858
+ async function parseMigrations(migrationsPath, rootPath) {
859
+ const files = await findFiles("*.cs", { cwd: migrationsPath });
860
+ const migrations = [];
861
+ const pattern = /^(\d{8})_(\d{3})_(.+)\.cs$/;
862
+ for (const file of files) {
863
+ const fileName = path6.basename(file);
864
+ if (fileName.includes(".Designer.") || fileName.includes("ModelSnapshot")) {
865
+ continue;
866
+ }
867
+ const match = pattern.exec(fileName);
868
+ if (match) {
869
+ migrations.push({
870
+ name: fileName.replace(".cs", ""),
871
+ timestamp: match[1],
872
+ prefix: match[2],
873
+ // Now this is the sequence number (NNN)
874
+ description: match[3],
875
+ file: path6.relative(rootPath, file),
876
+ applied: true
877
+ // We'd need DB connection to check this
878
+ });
879
+ } else {
880
+ migrations.push({
881
+ name: fileName.replace(".cs", ""),
882
+ timestamp: "",
883
+ prefix: "Unknown",
884
+ description: fileName.replace(".cs", ""),
885
+ file: path6.relative(rootPath, file),
886
+ applied: true
887
+ });
888
+ }
889
+ }
890
+ return migrations.sort((a, b) => {
891
+ const dateCompare = a.timestamp.localeCompare(b.timestamp);
892
+ if (dateCompare !== 0) return dateCompare;
893
+ return a.prefix.localeCompare(b.prefix);
894
+ });
895
+ }
896
+ function checkNamingConventions(result, _config) {
897
+ for (const migration of result.migrations) {
898
+ if (!migration.timestamp) {
899
+ result.conflicts.push({
900
+ type: "naming",
901
+ description: `Migration "${migration.name}" does not follow naming convention`,
902
+ files: [migration.file],
903
+ resolution: `Rename to format: YYYYMMDD_NNN_Description (e.g., 20260115_001_InitialCreate)`
904
+ });
905
+ }
906
+ if (migration.prefix === "Unknown") {
907
+ result.conflicts.push({
908
+ type: "naming",
909
+ description: `Migration "${migration.name}" missing sequence number`,
910
+ files: [migration.file],
911
+ resolution: `Use format: YYYYMMDD_NNN_Description where NNN is a 3-digit sequence (001, 002, etc.)`
912
+ });
913
+ }
914
+ }
915
+ }
916
+ function checkChronologicalOrder(result) {
917
+ const migrations = result.migrations.filter((m) => m.timestamp);
918
+ for (let i = 1; i < migrations.length; i++) {
919
+ const prev = migrations[i - 1];
920
+ const curr = migrations[i];
921
+ if (curr.timestamp < prev.timestamp) {
922
+ result.conflicts.push({
923
+ type: "order",
924
+ description: `Migration "${curr.name}" (${curr.timestamp}) is dated before "${prev.name}" (${prev.timestamp})`,
925
+ files: [curr.file, prev.file],
926
+ resolution: "Reorder migrations or update timestamps"
927
+ });
928
+ }
929
+ if (curr.timestamp === prev.timestamp && curr.prefix === prev.prefix) {
930
+ result.conflicts.push({
931
+ type: "order",
932
+ description: `Migrations "${curr.name}" and "${prev.name}" have same timestamp`,
933
+ files: [curr.file, prev.file],
934
+ resolution: "Use different sequence numbers (NNN) or different dates"
935
+ });
936
+ }
937
+ }
938
+ }
939
+ async function checkBranchConflicts(result, structure, currentBranch, compareBranch, projectPath) {
940
+ if (!structure.migrations) return;
941
+ const migrationsRelPath = path6.relative(projectPath, structure.migrations).replace(/\\/g, "/");
942
+ const snapshotFiles = await findFiles("*ModelSnapshot.cs", { cwd: structure.migrations });
943
+ if (snapshotFiles.length > 0) {
944
+ const snapshotRelPath = path6.relative(projectPath, snapshotFiles[0]).replace(/\\/g, "/");
945
+ const currentSnapshot = await readText(snapshotFiles[0]);
946
+ const compareSnapshot = await getFileFromBranch(compareBranch, snapshotRelPath, projectPath);
947
+ if (compareSnapshot && currentSnapshot !== compareSnapshot) {
948
+ const diff = await getDiff(compareBranch, currentBranch, snapshotRelPath, projectPath);
949
+ if (diff) {
950
+ result.conflicts.push({
951
+ type: "snapshot",
952
+ description: `ModelSnapshot differs between "${currentBranch}" and "${compareBranch}"`,
953
+ files: [snapshotRelPath],
954
+ resolution: "Rebase on target branch and regenerate migrations with: dotnet ef migrations add <Name>"
955
+ });
956
+ }
957
+ }
958
+ }
959
+ const compareMigrations = await getFileFromBranch(
960
+ compareBranch,
961
+ migrationsRelPath,
962
+ projectPath
963
+ );
964
+ if (compareMigrations) {
965
+ logger.debug("Branch comparison completed", { compareBranch });
966
+ }
967
+ }
968
+ async function checkModelSnapshot(result, structure) {
969
+ if (!structure.migrations) return;
970
+ const snapshotFiles = await findFiles("*ModelSnapshot.cs", { cwd: structure.migrations });
971
+ if (snapshotFiles.length === 0) {
972
+ result.conflicts.push({
973
+ type: "snapshot",
974
+ description: "No ModelSnapshot file found",
975
+ files: [],
976
+ resolution: "Run: dotnet ef migrations add InitialCreate"
977
+ });
978
+ return;
979
+ }
980
+ if (snapshotFiles.length > 1) {
981
+ result.conflicts.push({
982
+ type: "snapshot",
983
+ description: "Multiple ModelSnapshot files found",
984
+ files: snapshotFiles.map((f) => path6.relative(structure.root, f)),
985
+ resolution: "Remove duplicate snapshots, keep only one per DbContext"
986
+ });
987
+ }
988
+ const snapshotContent = await readText(snapshotFiles[0]);
989
+ for (const migration of result.migrations) {
990
+ if (migration.timestamp && !snapshotContent.includes(migration.name)) {
991
+ result.conflicts.push({
992
+ type: "dependency",
993
+ description: `Migration "${migration.name}" not referenced in ModelSnapshot`,
994
+ files: [migration.file],
995
+ resolution: "Migration may not be applied. Run: dotnet ef database update"
996
+ });
997
+ }
998
+ }
999
+ }
1000
+ function generateSuggestions(result) {
1001
+ if (result.conflicts.some((c) => c.type === "snapshot")) {
1002
+ result.suggestions.push(
1003
+ "Consider rebasing on the target branch before merging to avoid snapshot conflicts"
1004
+ );
1005
+ }
1006
+ if (result.conflicts.some((c) => c.type === "naming")) {
1007
+ result.suggestions.push(
1008
+ "Use convention: YYYYMMDD_NNN_Description for migration naming (e.g., 20260115_001_InitialCreate)"
1009
+ );
1010
+ }
1011
+ if (result.conflicts.some((c) => c.type === "order")) {
1012
+ result.suggestions.push(
1013
+ "Ensure migrations are created in chronological order to avoid conflicts"
1014
+ );
1015
+ }
1016
+ if (result.migrations.length > 20) {
1017
+ result.suggestions.push(
1018
+ "Consider squashing old migrations to reduce complexity. Use: /efcore squash"
1019
+ );
1020
+ }
1021
+ }
1022
+ function formatResult2(result, currentBranch, compareBranch) {
1023
+ const lines = [];
1024
+ lines.push("# EF Core Migration Check Report");
1025
+ lines.push("");
1026
+ lines.push("## Overview");
1027
+ lines.push(`- **Current Branch**: ${currentBranch}`);
1028
+ if (compareBranch) {
1029
+ lines.push(`- **Compare Branch**: ${compareBranch}`);
1030
+ }
1031
+ lines.push(`- **Total Migrations**: ${result.migrations.length}`);
1032
+ lines.push(`- **Conflicts Found**: ${result.conflicts.length}`);
1033
+ lines.push(`- **Status**: ${result.hasConflicts ? "\u274C CONFLICTS DETECTED" : "\u2705 OK"}`);
1034
+ lines.push("");
1035
+ lines.push("## Migrations");
1036
+ lines.push("");
1037
+ lines.push("| Name | Timestamp | Prefix | Description |");
1038
+ lines.push("|------|-----------|--------|-------------|");
1039
+ for (const migration of result.migrations) {
1040
+ lines.push(
1041
+ `| ${migration.name} | ${migration.timestamp || "N/A"} | ${migration.prefix} | ${migration.description} |`
1042
+ );
1043
+ }
1044
+ lines.push("");
1045
+ if (result.conflicts.length > 0) {
1046
+ lines.push("## Conflicts");
1047
+ lines.push("");
1048
+ for (const conflict of result.conflicts) {
1049
+ const icon = conflict.type === "snapshot" ? "\u{1F504}" : conflict.type === "order" ? "\u{1F4C5}" : conflict.type === "naming" ? "\u{1F4DD}" : "\u26A0\uFE0F";
1050
+ lines.push(`### ${icon} ${conflict.type.toUpperCase()}: ${conflict.description}`);
1051
+ if (conflict.files.length > 0) {
1052
+ lines.push(`- **Files**: ${conflict.files.map((f) => `\`${f}\``).join(", ")}`);
1053
+ }
1054
+ lines.push(`- **Resolution**: ${conflict.resolution}`);
1055
+ lines.push("");
1056
+ }
1057
+ }
1058
+ if (result.suggestions.length > 0) {
1059
+ lines.push("## Suggestions");
1060
+ lines.push("");
1061
+ for (const suggestion of result.suggestions) {
1062
+ lines.push(`- \u{1F4A1} ${suggestion}`);
1063
+ }
1064
+ lines.push("");
1065
+ }
1066
+ return lines.join("\n");
1067
+ }
1068
+
1069
+ // src/tools/scaffold-extension.ts
1070
+ import Handlebars from "handlebars";
1071
+ import path7 from "path";
1072
+ var scaffoldExtensionTool = {
1073
+ name: "scaffold_extension",
1074
+ description: "Generate code to extend SmartStack: service (interface + implementation), entity (class + EF config), controller (REST endpoints), or React component",
1075
+ inputSchema: {
1076
+ type: "object",
1077
+ properties: {
1078
+ type: {
1079
+ type: "string",
1080
+ enum: ["service", "entity", "controller", "component"],
1081
+ description: "Type of extension to scaffold"
1082
+ },
1083
+ name: {
1084
+ type: "string",
1085
+ description: 'Name of the extension (e.g., "UserProfile", "Order")'
1086
+ },
1087
+ options: {
1088
+ type: "object",
1089
+ properties: {
1090
+ namespace: {
1091
+ type: "string",
1092
+ description: "Custom namespace (optional)"
1093
+ },
1094
+ baseEntity: {
1095
+ type: "string",
1096
+ description: "Base entity to extend (for entity type)"
1097
+ },
1098
+ methods: {
1099
+ type: "array",
1100
+ items: { type: "string" },
1101
+ description: "Methods to generate (for service type)"
1102
+ },
1103
+ outputPath: {
1104
+ type: "string",
1105
+ description: "Custom output path"
1106
+ }
1107
+ }
1108
+ }
1109
+ },
1110
+ required: ["type", "name"]
1111
+ }
1112
+ };
1113
+ Handlebars.registerHelper("pascalCase", (str) => {
1114
+ return str.charAt(0).toUpperCase() + str.slice(1);
1115
+ });
1116
+ Handlebars.registerHelper("camelCase", (str) => {
1117
+ return str.charAt(0).toLowerCase() + str.slice(1);
1118
+ });
1119
+ Handlebars.registerHelper("kebabCase", (str) => {
1120
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
1121
+ });
1122
+ async function handleScaffoldExtension(args, config) {
1123
+ const input = ScaffoldExtensionInputSchema.parse(args);
1124
+ logger.info("Scaffolding extension", { type: input.type, name: input.name });
1125
+ const structure = await findSmartStackStructure(config.smartstack.projectPath);
1126
+ const result = {
1127
+ success: true,
1128
+ files: [],
1129
+ instructions: []
1130
+ };
1131
+ try {
1132
+ switch (input.type) {
1133
+ case "service":
1134
+ await scaffoldService(input.name, input.options, structure, config, result);
1135
+ break;
1136
+ case "entity":
1137
+ await scaffoldEntity(input.name, input.options, structure, config, result);
1138
+ break;
1139
+ case "controller":
1140
+ await scaffoldController(input.name, input.options, structure, config, result);
1141
+ break;
1142
+ case "component":
1143
+ await scaffoldComponent(input.name, input.options, structure, config, result);
1144
+ break;
1145
+ }
1146
+ } catch (error) {
1147
+ result.success = false;
1148
+ result.instructions.push(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1149
+ }
1150
+ return formatResult3(result, input.type, input.name);
1151
+ }
1152
+ async function scaffoldService(name, options, structure, config, result) {
1153
+ const namespace = options?.namespace || `${config.conventions.namespaces.application}.Services`;
1154
+ const methods = options?.methods || ["GetByIdAsync", "GetAllAsync", "CreateAsync", "UpdateAsync", "DeleteAsync"];
1155
+ const interfaceTemplate = `using System.Threading;
1156
+ using System.Threading.Tasks;
1157
+ using System.Collections.Generic;
1158
+
1159
+ namespace {{namespace}};
1160
+
1161
+ /// <summary>
1162
+ /// Service interface for {{name}} operations
1163
+ /// </summary>
1164
+ public interface I{{name}}Service
1165
+ {
1166
+ {{#each methods}}
1167
+ /// <summary>
1168
+ /// {{this}} operation
1169
+ /// </summary>
1170
+ Task<object> {{this}}(CancellationToken cancellationToken = default);
1171
+
1172
+ {{/each}}
1173
+ }
1174
+ `;
1175
+ const implementationTemplate = `using System.Threading;
1176
+ using System.Threading.Tasks;
1177
+ using System.Collections.Generic;
1178
+ using Microsoft.Extensions.Logging;
1179
+
1180
+ namespace {{namespace}};
1181
+
1182
+ /// <summary>
1183
+ /// Service implementation for {{name}} operations
1184
+ /// </summary>
1185
+ public class {{name}}Service : I{{name}}Service
1186
+ {
1187
+ private readonly ILogger<{{name}}Service> _logger;
1188
+
1189
+ public {{name}}Service(ILogger<{{name}}Service> logger)
1190
+ {
1191
+ _logger = logger;
1192
+ }
1193
+
1194
+ {{#each methods}}
1195
+ /// <inheritdoc />
1196
+ public async Task<object> {{this}}(CancellationToken cancellationToken = default)
1197
+ {
1198
+ _logger.LogInformation("Executing {{this}}");
1199
+ // TODO: Implement {{this}}
1200
+ await Task.CompletedTask;
1201
+ throw new NotImplementedException();
1202
+ }
1203
+
1204
+ {{/each}}
1205
+ }
1206
+ `;
1207
+ const diTemplate = `// Add to DependencyInjection.cs or ServiceCollectionExtensions.cs:
1208
+ services.AddScoped<I{{name}}Service, {{name}}Service>();
1209
+ `;
1210
+ const context = { namespace, name, methods };
1211
+ const interfaceContent = Handlebars.compile(interfaceTemplate)(context);
1212
+ const implementationContent = Handlebars.compile(implementationTemplate)(context);
1213
+ const diContent = Handlebars.compile(diTemplate)(context);
1214
+ const basePath = structure.application || config.smartstack.projectPath;
1215
+ const servicesPath = path7.join(basePath, "Services");
1216
+ await ensureDirectory(servicesPath);
1217
+ const interfacePath = path7.join(servicesPath, `I${name}Service.cs`);
1218
+ const implementationPath = path7.join(servicesPath, `${name}Service.cs`);
1219
+ await writeText(interfacePath, interfaceContent);
1220
+ result.files.push({ path: interfacePath, content: interfaceContent, type: "created" });
1221
+ await writeText(implementationPath, implementationContent);
1222
+ result.files.push({ path: implementationPath, content: implementationContent, type: "created" });
1223
+ result.instructions.push("Register service in DI container:");
1224
+ result.instructions.push(diContent);
1225
+ }
1226
+ async function scaffoldEntity(name, options, structure, config, result) {
1227
+ const namespace = options?.namespace || config.conventions.namespaces.domain;
1228
+ const baseEntity = options?.baseEntity;
1229
+ const entityTemplate = `using System;
1230
+
1231
+ namespace {{namespace}};
1232
+
1233
+ /// <summary>
1234
+ /// {{name}} entity{{#if baseEntity}} extending {{baseEntity}}{{/if}}
1235
+ /// </summary>
1236
+ public class {{name}}{{#if baseEntity}} : {{baseEntity}}{{/if}}
1237
+ {
1238
+ {{#unless baseEntity}}
1239
+ public Guid Id { get; set; }
1240
+
1241
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
1242
+
1243
+ public DateTime? UpdatedAt { get; set; }
1244
+
1245
+ {{/unless}}
1246
+ // TODO: Add {{name}} specific properties
1247
+ }
1248
+ `;
1249
+ const configTemplate = `using Microsoft.EntityFrameworkCore;
1250
+ using Microsoft.EntityFrameworkCore.Metadata.Builders;
1251
+
1252
+ namespace {{infrastructureNamespace}}.Persistence.Configurations;
1253
+
1254
+ public class {{name}}Configuration : IEntityTypeConfiguration<{{domainNamespace}}.{{name}}>
1255
+ {
1256
+ public void Configure(EntityTypeBuilder<{{domainNamespace}}.{{name}}> builder)
1257
+ {
1258
+ builder.ToTable("{{tablePrefix}}{{name}}s");
1259
+
1260
+ builder.HasKey(e => e.Id);
1261
+
1262
+ // TODO: Add {{name}} specific configuration
1263
+ }
1264
+ }
1265
+ `;
1266
+ const context = {
1267
+ namespace,
1268
+ name,
1269
+ baseEntity,
1270
+ infrastructureNamespace: config.conventions.namespaces.infrastructure,
1271
+ domainNamespace: config.conventions.namespaces.domain,
1272
+ tablePrefix: config.conventions.tablePrefix.core
1273
+ };
1274
+ const entityContent = Handlebars.compile(entityTemplate)(context);
1275
+ const configContent = Handlebars.compile(configTemplate)(context);
1276
+ const domainPath = structure.domain || path7.join(config.smartstack.projectPath, "Domain");
1277
+ const infraPath = structure.infrastructure || path7.join(config.smartstack.projectPath, "Infrastructure");
1278
+ await ensureDirectory(domainPath);
1279
+ await ensureDirectory(path7.join(infraPath, "Persistence", "Configurations"));
1280
+ const entityFilePath = path7.join(domainPath, `${name}.cs`);
1281
+ const configFilePath = path7.join(infraPath, "Persistence", "Configurations", `${name}Configuration.cs`);
1282
+ await writeText(entityFilePath, entityContent);
1283
+ result.files.push({ path: entityFilePath, content: entityContent, type: "created" });
1284
+ await writeText(configFilePath, configContent);
1285
+ result.files.push({ path: configFilePath, content: configContent, type: "created" });
1286
+ result.instructions.push(`Add DbSet to ApplicationDbContext:`);
1287
+ result.instructions.push(`public DbSet<${name}> ${name}s => Set<${name}>();`);
1288
+ result.instructions.push("");
1289
+ result.instructions.push("Create migration:");
1290
+ result.instructions.push(`dotnet ef migrations add Add${name}`);
1291
+ }
1292
+ async function scaffoldController(name, options, structure, config, result) {
1293
+ const namespace = options?.namespace || `${config.conventions.namespaces.api}.Controllers`;
1294
+ const controllerTemplate = `using Microsoft.AspNetCore.Authorization;
1295
+ using Microsoft.AspNetCore.Mvc;
1296
+ using Microsoft.Extensions.Logging;
1297
+
1298
+ namespace {{namespace}};
1299
+
1300
+ /// <summary>
1301
+ /// API controller for {{name}} operations
1302
+ /// </summary>
1303
+ [ApiController]
1304
+ [Route("api/[controller]")]
1305
+ [Authorize]
1306
+ public class {{name}}Controller : ControllerBase
1307
+ {
1308
+ private readonly ILogger<{{name}}Controller> _logger;
1309
+
1310
+ public {{name}}Controller(ILogger<{{name}}Controller> logger)
1311
+ {
1312
+ _logger = logger;
1313
+ }
1314
+
1315
+ /// <summary>
1316
+ /// Get all {{nameLower}}s
1317
+ /// </summary>
1318
+ [HttpGet]
1319
+ public async Task<ActionResult<IEnumerable<{{name}}Dto>>> GetAll()
1320
+ {
1321
+ _logger.LogInformation("Getting all {{nameLower}}s");
1322
+ // TODO: Implement
1323
+ return Ok(Array.Empty<{{name}}Dto>());
1324
+ }
1325
+
1326
+ /// <summary>
1327
+ /// Get {{nameLower}} by ID
1328
+ /// </summary>
1329
+ [HttpGet("{id:guid}")]
1330
+ public async Task<ActionResult<{{name}}Dto>> GetById(Guid id)
1331
+ {
1332
+ _logger.LogInformation("Getting {{nameLower}} {Id}", id);
1333
+ // TODO: Implement
1334
+ return NotFound();
1335
+ }
1336
+
1337
+ /// <summary>
1338
+ /// Create new {{nameLower}}
1339
+ /// </summary>
1340
+ [HttpPost]
1341
+ public async Task<ActionResult<{{name}}Dto>> Create([FromBody] Create{{name}}Request request)
1342
+ {
1343
+ _logger.LogInformation("Creating {{nameLower}}");
1344
+ // TODO: Implement
1345
+ return CreatedAtAction(nameof(GetById), new { id = Guid.NewGuid() }, null);
1346
+ }
1347
+
1348
+ /// <summary>
1349
+ /// Update {{nameLower}}
1350
+ /// </summary>
1351
+ [HttpPut("{id:guid}")]
1352
+ public async Task<ActionResult> Update(Guid id, [FromBody] Update{{name}}Request request)
1353
+ {
1354
+ _logger.LogInformation("Updating {{nameLower}} {Id}", id);
1355
+ // TODO: Implement
1356
+ return NoContent();
1357
+ }
1358
+
1359
+ /// <summary>
1360
+ /// Delete {{nameLower}}
1361
+ /// </summary>
1362
+ [HttpDelete("{id:guid}")]
1363
+ public async Task<ActionResult> Delete(Guid id)
1364
+ {
1365
+ _logger.LogInformation("Deleting {{nameLower}} {Id}", id);
1366
+ // TODO: Implement
1367
+ return NoContent();
1368
+ }
1369
+ }
1370
+
1371
+ // DTOs
1372
+ public record {{name}}Dto(Guid Id, DateTime CreatedAt);
1373
+ public record Create{{name}}Request();
1374
+ public record Update{{name}}Request();
1375
+ `;
1376
+ const context = {
1377
+ namespace,
1378
+ name,
1379
+ nameLower: name.charAt(0).toLowerCase() + name.slice(1)
1380
+ };
1381
+ const controllerContent = Handlebars.compile(controllerTemplate)(context);
1382
+ const apiPath = structure.api || path7.join(config.smartstack.projectPath, "Api");
1383
+ const controllersPath = path7.join(apiPath, "Controllers");
1384
+ await ensureDirectory(controllersPath);
1385
+ const controllerFilePath = path7.join(controllersPath, `${name}Controller.cs`);
1386
+ await writeText(controllerFilePath, controllerContent);
1387
+ result.files.push({ path: controllerFilePath, content: controllerContent, type: "created" });
1388
+ result.instructions.push("Controller created. API endpoints:");
1389
+ result.instructions.push(` GET /api/${name.toLowerCase()}`);
1390
+ result.instructions.push(` GET /api/${name.toLowerCase()}/{id}`);
1391
+ result.instructions.push(` POST /api/${name.toLowerCase()}`);
1392
+ result.instructions.push(` PUT /api/${name.toLowerCase()}/{id}`);
1393
+ result.instructions.push(` DELETE /api/${name.toLowerCase()}/{id}`);
1394
+ }
1395
+ async function scaffoldComponent(name, options, structure, config, result) {
1396
+ const componentTemplate = `import React, { useState, useEffect } from 'react';
1397
+
1398
+ interface {{name}}Props {
1399
+ id?: string;
1400
+ onSave?: (data: {{name}}Data) => void;
1401
+ onCancel?: () => void;
1402
+ }
1403
+
1404
+ interface {{name}}Data {
1405
+ id?: string;
1406
+ // TODO: Add {{name}} data properties
1407
+ }
1408
+
1409
+ /**
1410
+ * {{name}} component
1411
+ */
1412
+ export const {{name}}: React.FC<{{name}}Props> = ({ id, onSave, onCancel }) => {
1413
+ const [data, setData] = useState<{{name}}Data | null>(null);
1414
+ const [loading, setLoading] = useState(false);
1415
+ const [error, setError] = useState<string | null>(null);
1416
+
1417
+ useEffect(() => {
1418
+ if (id) {
1419
+ // TODO: Fetch {{nameLower}} data
1420
+ setLoading(true);
1421
+ // fetch...
1422
+ setLoading(false);
1423
+ }
1424
+ }, [id]);
1425
+
1426
+ const handleSubmit = async (e: React.FormEvent) => {
1427
+ e.preventDefault();
1428
+ if (data && onSave) {
1429
+ onSave(data);
1430
+ }
1431
+ };
1432
+
1433
+ if (loading) {
1434
+ return <div className="animate-pulse">Loading...</div>;
1435
+ }
1436
+
1437
+ if (error) {
1438
+ return <div className="text-red-500">{error}</div>;
1439
+ }
1440
+
1441
+ return (
1442
+ <div className="p-4">
1443
+ <h2 className="text-xl font-semibold mb-4">{{name}}</h2>
1444
+ <form onSubmit={handleSubmit} className="space-y-4">
1445
+ {/* TODO: Add form fields */}
1446
+ <div className="flex gap-2">
1447
+ <button
1448
+ type="submit"
1449
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
1450
+ >
1451
+ Save
1452
+ </button>
1453
+ {onCancel && (
1454
+ <button
1455
+ type="button"
1456
+ onClick={onCancel}
1457
+ className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
1458
+ >
1459
+ Cancel
1460
+ </button>
1461
+ )}
1462
+ </div>
1463
+ </form>
1464
+ </div>
1465
+ );
1466
+ };
1467
+
1468
+ export default {{name}};
1469
+ `;
1470
+ const hookTemplate = `import { useState, useEffect, useCallback } from 'react';
1471
+ import { {{nameLower}}Api } from '../services/api/{{nameLower}}';
1472
+
1473
+ interface {{name}}Data {
1474
+ id?: string;
1475
+ // TODO: Add properties
1476
+ }
1477
+
1478
+ interface Use{{name}}Options {
1479
+ id?: string;
1480
+ autoFetch?: boolean;
1481
+ }
1482
+
1483
+ export function use{{name}}(options: Use{{name}}Options = {}) {
1484
+ const { id, autoFetch = true } = options;
1485
+ const [data, setData] = useState<{{name}}Data | null>(null);
1486
+ const [loading, setLoading] = useState(false);
1487
+ const [error, setError] = useState<Error | null>(null);
1488
+
1489
+ const fetch{{name}} = useCallback(async (fetchId?: string) => {
1490
+ const targetId = fetchId || id;
1491
+ if (!targetId) return;
1492
+
1493
+ setLoading(true);
1494
+ setError(null);
1495
+ try {
1496
+ // TODO: Implement API call
1497
+ // const result = await {{nameLower}}Api.getById(targetId);
1498
+ // setData(result);
1499
+ } catch (e) {
1500
+ setError(e instanceof Error ? e : new Error('Unknown error'));
1501
+ } finally {
1502
+ setLoading(false);
1503
+ }
1504
+ }, [id]);
1505
+
1506
+ const save{{name}} = useCallback(async (saveData: {{name}}Data) => {
1507
+ setLoading(true);
1508
+ setError(null);
1509
+ try {
1510
+ // TODO: Implement API call
1511
+ // const result = saveData.id
1512
+ // ? await {{nameLower}}Api.update(saveData.id, saveData)
1513
+ // : await {{nameLower}}Api.create(saveData);
1514
+ // setData(result);
1515
+ // return result;
1516
+ } catch (e) {
1517
+ setError(e instanceof Error ? e : new Error('Unknown error'));
1518
+ throw e;
1519
+ } finally {
1520
+ setLoading(false);
1521
+ }
1522
+ }, []);
1523
+
1524
+ useEffect(() => {
1525
+ if (autoFetch && id) {
1526
+ fetch{{name}}();
1527
+ }
1528
+ }, [autoFetch, id, fetch{{name}}]);
1529
+
1530
+ return {
1531
+ data,
1532
+ loading,
1533
+ error,
1534
+ fetch: fetch{{name}},
1535
+ save: save{{name}},
1536
+ setData,
1537
+ };
1538
+ }
1539
+ `;
1540
+ const context = {
1541
+ name,
1542
+ nameLower: name.charAt(0).toLowerCase() + name.slice(1)
1543
+ };
1544
+ const componentContent = Handlebars.compile(componentTemplate)(context);
1545
+ const hookContent = Handlebars.compile(hookTemplate)(context);
1546
+ const webPath = structure.web || path7.join(config.smartstack.projectPath, "web", "smartstack-web");
1547
+ const componentsPath = options?.outputPath || path7.join(webPath, "src", "components");
1548
+ const hooksPath = path7.join(webPath, "src", "hooks");
1549
+ await ensureDirectory(componentsPath);
1550
+ await ensureDirectory(hooksPath);
1551
+ const componentFilePath = path7.join(componentsPath, `${name}.tsx`);
1552
+ const hookFilePath = path7.join(hooksPath, `use${name}.ts`);
1553
+ await writeText(componentFilePath, componentContent);
1554
+ result.files.push({ path: componentFilePath, content: componentContent, type: "created" });
1555
+ await writeText(hookFilePath, hookContent);
1556
+ result.files.push({ path: hookFilePath, content: hookContent, type: "created" });
1557
+ result.instructions.push("Import and use the component:");
1558
+ result.instructions.push(`import { ${name} } from './components/${name}';`);
1559
+ result.instructions.push(`import { use${name} } from './hooks/use${name}';`);
1560
+ }
1561
+ function formatResult3(result, type, name) {
1562
+ const lines = [];
1563
+ lines.push(`# Scaffold ${type}: ${name}`);
1564
+ lines.push("");
1565
+ if (result.success) {
1566
+ lines.push("## \u2705 Files Generated");
1567
+ lines.push("");
1568
+ for (const file of result.files) {
1569
+ lines.push(`### ${file.type === "created" ? "\u{1F4C4}" : "\u270F\uFE0F"} ${path7.basename(file.path)}`);
1570
+ lines.push(`**Path**: \`${file.path}\``);
1571
+ lines.push("");
1572
+ lines.push("```" + (file.path.endsWith(".cs") ? "csharp" : "typescript"));
1573
+ const contentLines = file.content.split("\n").slice(0, 50);
1574
+ lines.push(contentLines.join("\n"));
1575
+ if (file.content.split("\n").length > 50) {
1576
+ lines.push("// ... (truncated)");
1577
+ }
1578
+ lines.push("```");
1579
+ lines.push("");
1580
+ }
1581
+ if (result.instructions.length > 0) {
1582
+ lines.push("## \u{1F4CB} Next Steps");
1583
+ lines.push("");
1584
+ for (const instruction of result.instructions) {
1585
+ if (instruction.startsWith("services.") || instruction.startsWith("public DbSet")) {
1586
+ lines.push("```csharp");
1587
+ lines.push(instruction);
1588
+ lines.push("```");
1589
+ } else if (instruction.startsWith("dotnet ")) {
1590
+ lines.push("```bash");
1591
+ lines.push(instruction);
1592
+ lines.push("```");
1593
+ } else if (instruction.startsWith("import ")) {
1594
+ lines.push("```typescript");
1595
+ lines.push(instruction);
1596
+ lines.push("```");
1597
+ } else {
1598
+ lines.push(instruction);
1599
+ }
1600
+ }
1601
+ }
1602
+ } else {
1603
+ lines.push("## \u274C Generation Failed");
1604
+ lines.push("");
1605
+ for (const instruction of result.instructions) {
1606
+ lines.push(`- ${instruction}`);
1607
+ }
1608
+ }
1609
+ return lines.join("\n");
1610
+ }
1611
+
1612
+ // src/tools/api-docs.ts
1613
+ import axios from "axios";
1614
+ import path8 from "path";
1615
+ var apiDocsTool = {
1616
+ name: "api_docs",
1617
+ description: "Get API documentation for SmartStack endpoints. Can fetch from Swagger/OpenAPI or parse controller files directly.",
1618
+ inputSchema: {
1619
+ type: "object",
1620
+ properties: {
1621
+ endpoint: {
1622
+ type: "string",
1623
+ description: 'Filter by endpoint path (e.g., "/api/users"). Leave empty for all endpoints.'
1624
+ },
1625
+ format: {
1626
+ type: "string",
1627
+ enum: ["markdown", "json", "openapi"],
1628
+ description: "Output format",
1629
+ default: "markdown"
1630
+ },
1631
+ controller: {
1632
+ type: "string",
1633
+ description: 'Filter by controller name (e.g., "Users")'
1634
+ }
1635
+ }
1636
+ }
1637
+ };
1638
+ async function handleApiDocs(args, config) {
1639
+ const input = ApiDocsInputSchema.parse(args);
1640
+ logger.info("Fetching API documentation", { endpoint: input.endpoint, format: input.format });
1641
+ let endpoints = [];
1642
+ if (config.smartstack.apiEnabled && config.smartstack.apiUrl) {
1643
+ try {
1644
+ endpoints = await fetchFromSwagger(config.smartstack.apiUrl);
1645
+ logger.debug("Fetched endpoints from Swagger", { count: endpoints.length });
1646
+ } catch (error) {
1647
+ logger.warn("Failed to fetch from Swagger, falling back to code parsing", { error });
1648
+ }
1649
+ }
1650
+ if (endpoints.length === 0) {
1651
+ const structure = await findSmartStackStructure(config.smartstack.projectPath);
1652
+ endpoints = await parseControllers(structure);
1653
+ }
1654
+ if (input.endpoint) {
1655
+ const filter = input.endpoint.toLowerCase();
1656
+ endpoints = endpoints.filter((e) => e.path.toLowerCase().includes(filter));
1657
+ }
1658
+ if (input.controller) {
1659
+ const filter = input.controller.toLowerCase();
1660
+ endpoints = endpoints.filter((e) => e.controller.toLowerCase().includes(filter));
1661
+ }
1662
+ switch (input.format) {
1663
+ case "json":
1664
+ return JSON.stringify(endpoints, null, 2);
1665
+ case "openapi":
1666
+ return formatAsOpenApi(endpoints);
1667
+ case "markdown":
1668
+ default:
1669
+ return formatAsMarkdown(endpoints);
1670
+ }
1671
+ }
1672
+ async function fetchFromSwagger(apiUrl) {
1673
+ const swaggerUrl = `${apiUrl}/swagger/v1/swagger.json`;
1674
+ const response = await axios.get(swaggerUrl, {
1675
+ timeout: 5e3,
1676
+ httpsAgent: new (await import("https")).Agent({ rejectUnauthorized: false })
1677
+ });
1678
+ const spec = response.data;
1679
+ const endpoints = [];
1680
+ for (const [pathKey, pathItem] of Object.entries(spec.paths || {})) {
1681
+ const pathObj = pathItem;
1682
+ for (const method of ["get", "post", "put", "patch", "delete"]) {
1683
+ const operation = pathObj[method];
1684
+ if (!operation) continue;
1685
+ const tags = operation.tags || ["Unknown"];
1686
+ const parameters = operation.parameters || [];
1687
+ endpoints.push({
1688
+ method: method.toUpperCase(),
1689
+ path: pathKey,
1690
+ controller: tags[0],
1691
+ action: operation.operationId || method,
1692
+ summary: operation.summary,
1693
+ parameters: parameters.map((p) => ({
1694
+ name: p.name,
1695
+ in: p.in,
1696
+ type: p.schema?.type || "string",
1697
+ required: p.required || false,
1698
+ description: p.description
1699
+ })),
1700
+ requestBody: operation.requestBody ? "See schema" : void 0,
1701
+ responses: Object.entries(operation.responses || {}).map(([status, resp]) => ({
1702
+ status: parseInt(status, 10),
1703
+ description: resp.description
1704
+ })),
1705
+ authorize: !!(operation.security && operation.security.length > 0)
1706
+ });
1707
+ }
1708
+ }
1709
+ return endpoints;
1710
+ }
1711
+ async function parseControllers(structure) {
1712
+ if (!structure.api) {
1713
+ return [];
1714
+ }
1715
+ const controllerFiles = await findControllerFiles(structure.api);
1716
+ const endpoints = [];
1717
+ for (const file of controllerFiles) {
1718
+ const content = await readText(file);
1719
+ const fileName = path8.basename(file, ".cs");
1720
+ const controllerName = fileName.replace("Controller", "");
1721
+ const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
1722
+ const baseRoute = routeMatch ? routeMatch[1].replace("[controller]", controllerName.toLowerCase()) : `/api/${controllerName.toLowerCase()}`;
1723
+ const hasAuthorize = content.includes("[Authorize]");
1724
+ const httpMethods = [
1725
+ { pattern: /\[HttpGet(?:\s*\(\s*"([^"]*)"\s*\))?\]/g, method: "GET" },
1726
+ { pattern: /\[HttpPost(?:\s*\(\s*"([^"]*)"\s*\))?\]/g, method: "POST" },
1727
+ { pattern: /\[HttpPut(?:\s*\(\s*"([^"]*)"\s*\))?\]/g, method: "PUT" },
1728
+ { pattern: /\[HttpPatch(?:\s*\(\s*"([^"]*)"\s*\))?\]/g, method: "PATCH" },
1729
+ { pattern: /\[HttpDelete(?:\s*\(\s*"([^"]*)"\s*\))?\]/g, method: "DELETE" }
1730
+ ];
1731
+ for (const { pattern, method } of httpMethods) {
1732
+ let match;
1733
+ while ((match = pattern.exec(content)) !== null) {
1734
+ const routeSuffix = match[1] || "";
1735
+ const fullPath = routeSuffix ? `${baseRoute}/${routeSuffix}` : baseRoute;
1736
+ const afterAttribute = content.substring(match.index);
1737
+ const methodMatch = afterAttribute.match(/public\s+(?:async\s+)?(?:Task<)?(?:ActionResult<)?(\w+)(?:>)?\s+(\w+)\s*\(/);
1738
+ const parameters = [];
1739
+ const pathParams = fullPath.match(/\{(\w+)(?::\w+)?\}/g);
1740
+ if (pathParams) {
1741
+ for (const param of pathParams) {
1742
+ const paramName = param.replace(/[{}:]/g, "").replace(/\w+$/, "");
1743
+ parameters.push({
1744
+ name: paramName || param.replace(/[{}]/g, "").split(":")[0],
1745
+ in: "path",
1746
+ type: "string",
1747
+ required: true
1748
+ });
1749
+ }
1750
+ }
1751
+ if (afterAttribute.includes("[FromBody]")) {
1752
+ const bodyMatch = afterAttribute.match(/\[FromBody\]\s*(\w+)\s+(\w+)/);
1753
+ if (bodyMatch) {
1754
+ parameters.push({
1755
+ name: bodyMatch[2],
1756
+ in: "body",
1757
+ type: bodyMatch[1],
1758
+ required: true
1759
+ });
1760
+ }
1761
+ }
1762
+ const queryMatches = afterAttribute.matchAll(/\[FromQuery\]\s*(\w+)\s+(\w+)/g);
1763
+ for (const qm of queryMatches) {
1764
+ parameters.push({
1765
+ name: qm[2],
1766
+ in: "query",
1767
+ type: qm[1],
1768
+ required: false
1769
+ });
1770
+ }
1771
+ endpoints.push({
1772
+ method,
1773
+ path: fullPath.replace(/\/+/g, "/"),
1774
+ controller: controllerName,
1775
+ action: methodMatch ? methodMatch[2] : "Unknown",
1776
+ parameters,
1777
+ responses: [{ status: 200, description: "Success" }],
1778
+ authorize: hasAuthorize
1779
+ });
1780
+ }
1781
+ }
1782
+ }
1783
+ return endpoints;
1784
+ }
1785
+ function formatAsMarkdown(endpoints) {
1786
+ const lines = [];
1787
+ lines.push("# SmartStack API Documentation");
1788
+ lines.push("");
1789
+ lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
1790
+ lines.push("");
1791
+ const byController = /* @__PURE__ */ new Map();
1792
+ for (const endpoint of endpoints) {
1793
+ const existing = byController.get(endpoint.controller) || [];
1794
+ existing.push(endpoint);
1795
+ byController.set(endpoint.controller, existing);
1796
+ }
1797
+ for (const [controller, controllerEndpoints] of byController) {
1798
+ lines.push(`## ${controller}`);
1799
+ lines.push("");
1800
+ for (const endpoint of controllerEndpoints) {
1801
+ const authBadge = endpoint.authorize ? " \u{1F512}" : "";
1802
+ lines.push(`### \`${endpoint.method}\` ${endpoint.path}${authBadge}`);
1803
+ lines.push("");
1804
+ if (endpoint.summary) {
1805
+ lines.push(endpoint.summary);
1806
+ lines.push("");
1807
+ }
1808
+ if (endpoint.parameters.length > 0) {
1809
+ lines.push("**Parameters:**");
1810
+ lines.push("");
1811
+ lines.push("| Name | In | Type | Required |");
1812
+ lines.push("|------|-----|------|----------|");
1813
+ for (const param of endpoint.parameters) {
1814
+ lines.push(`| ${param.name} | ${param.in} | ${param.type} | ${param.required ? "Yes" : "No"} |`);
1815
+ }
1816
+ lines.push("");
1817
+ }
1818
+ if (endpoint.responses.length > 0) {
1819
+ lines.push("**Responses:**");
1820
+ lines.push("");
1821
+ for (const resp of endpoint.responses) {
1822
+ lines.push(`- \`${resp.status}\`: ${resp.description || "No description"}`);
1823
+ }
1824
+ lines.push("");
1825
+ }
1826
+ }
1827
+ }
1828
+ if (endpoints.length === 0) {
1829
+ lines.push("No endpoints found matching the filter criteria.");
1830
+ }
1831
+ return lines.join("\n");
1832
+ }
1833
+ function formatAsOpenApi(endpoints) {
1834
+ const spec = {
1835
+ openapi: "3.0.0",
1836
+ info: {
1837
+ title: "SmartStack API",
1838
+ version: "1.0.0"
1839
+ },
1840
+ paths: {}
1841
+ };
1842
+ for (const endpoint of endpoints) {
1843
+ if (!spec.paths[endpoint.path]) {
1844
+ spec.paths[endpoint.path] = {};
1845
+ }
1846
+ spec.paths[endpoint.path][endpoint.method.toLowerCase()] = {
1847
+ tags: [endpoint.controller],
1848
+ operationId: endpoint.action,
1849
+ summary: endpoint.summary,
1850
+ parameters: endpoint.parameters.filter((p) => p.in !== "body").map((p) => ({
1851
+ name: p.name,
1852
+ in: p.in,
1853
+ required: p.required,
1854
+ schema: { type: p.type }
1855
+ })),
1856
+ responses: Object.fromEntries(
1857
+ endpoint.responses.map((r) => [
1858
+ r.status.toString(),
1859
+ { description: r.description || "Response" }
1860
+ ])
1861
+ ),
1862
+ security: endpoint.authorize ? [{ bearerAuth: [] }] : void 0
1863
+ };
1864
+ }
1865
+ return JSON.stringify(spec, null, 2);
1866
+ }
1867
+
1868
+ // src/resources/conventions.ts
1869
+ var conventionsResourceTemplate = {
1870
+ uri: "smartstack://conventions",
1871
+ name: "AtlasHub Conventions",
1872
+ description: "Documentation of AtlasHub/SmartStack naming conventions, patterns, and best practices",
1873
+ mimeType: "text/markdown"
1874
+ };
1875
+ async function getConventionsResource(config) {
1876
+ const { schemas, tablePrefixes, migrationFormat, namespaces, servicePattern } = config.conventions;
1877
+ return `# AtlasHub SmartStack Conventions
1878
+
1879
+ ## Overview
1880
+
1881
+ This document describes the mandatory conventions for extending the SmartStack/AtlasHub platform.
1882
+ Following these conventions ensures compatibility and prevents conflicts.
1883
+
1884
+ ---
1885
+
1886
+ ## 1. Database Conventions
1887
+
1888
+ ### SQL Schemas
1889
+
1890
+ SmartStack uses SQL Server schemas to separate platform tables from client extensions:
1891
+
1892
+ | Schema | Usage | Description |
1893
+ |--------|-------|-------------|
1894
+ | \`${schemas.platform}\` | SmartStack platform | All native SmartStack tables |
1895
+ | \`${schemas.extensions}\` | Client extensions | Custom tables added by clients |
1896
+
1897
+ ### Domain Table Prefixes
1898
+
1899
+ Tables are organized by domain using prefixes:
1900
+
1901
+ | Prefix | Domain | Example Tables |
1902
+ |--------|--------|----------------|
1903
+ | \`auth_\` | Authorization | auth_Users, auth_Roles, auth_Permissions |
1904
+ | \`nav_\` | Navigation | nav_Contexts, nav_Applications, nav_Modules |
1905
+ | \`usr_\` | User profiles | usr_Profiles, usr_Preferences |
1906
+ | \`ai_\` | AI features | ai_Providers, ai_Models, ai_Prompts |
1907
+ | \`cfg_\` | Configuration | cfg_Settings |
1908
+ | \`wkf_\` | Workflows | wkf_EmailTemplates, wkf_Workflows |
1909
+ | \`support_\` | Support | support_Tickets, support_Comments |
1910
+ | \`entra_\` | Entra sync | entra_Groups, entra_SyncState |
1911
+ | \`ref_\` | References | ref_Companies, ref_Departments |
1912
+ | \`loc_\` | Localization | loc_Languages, loc_Translations |
1913
+ | \`lic_\` | Licensing | lic_Licenses |
1914
+
1915
+ ### Entity Configuration
1916
+
1917
+ \`\`\`csharp
1918
+ public class MyEntityConfiguration : IEntityTypeConfiguration<MyEntity>
1919
+ {
1920
+ public void Configure(EntityTypeBuilder<MyEntity> builder)
1921
+ {
1922
+ // CORRECT: Use schema + domain prefix
1923
+ builder.ToTable("auth_Users", "${schemas.platform}");
1924
+
1925
+ // WRONG: No schema specified
1926
+ // builder.ToTable("auth_Users");
1927
+
1928
+ // WRONG: No domain prefix
1929
+ // builder.ToTable("Users", "${schemas.platform}");
1930
+ }
1931
+ }
1932
+ \`\`\`
1933
+
1934
+ ### Extending Core Entities
1935
+
1936
+ When extending a core entity, use the \`${schemas.extensions}\` schema:
1937
+
1938
+ \`\`\`csharp
1939
+ // Client extension for auth_Users
1940
+ public class UserExtension
1941
+ {
1942
+ public Guid UserId { get; set; } // FK to auth_Users
1943
+ public User User { get; set; }
1944
+
1945
+ // Custom properties
1946
+ public string CustomField { get; set; }
1947
+ }
1948
+
1949
+ // Configuration - use extensions schema
1950
+ builder.ToTable("client_UserExtensions", "${schemas.extensions}");
1951
+ builder.HasOne(e => e.User)
1952
+ .WithOne()
1953
+ .HasForeignKey<UserExtension>(e => e.UserId);
1954
+ \`\`\`
1955
+
1956
+ ---
1957
+
1958
+ ## 2. Migration Conventions
1959
+
1960
+ ### Naming Format
1961
+
1962
+ Migrations MUST follow this naming pattern:
1963
+
1964
+ \`\`\`
1965
+ ${migrationFormat}
1966
+ \`\`\`
1967
+
1968
+ **Examples:**
1969
+ - \`20260115_001_InitialSchema.cs\`
1970
+ - \`20260120_002_AddUserProfiles.cs\`
1971
+ - \`20260125_003_AddSupportTickets.cs\`
1972
+
1973
+ ### Creating Migrations
1974
+
1975
+ \`\`\`bash
1976
+ # Create a new migration
1977
+ dotnet ef migrations add 20260115_001_InitialSchema
1978
+
1979
+ # With context specified
1980
+ dotnet ef migrations add 20260115_001_InitialSchema --context ApplicationDbContext
1981
+ \`\`\`
1982
+
1983
+ ### Migration Rules
1984
+
1985
+ 1. **One migration per feature** - Group related changes in a single migration
1986
+ 2. **Timestamps ensure order** - Use YYYYMMDD format for chronological sorting
1987
+ 3. **Sequence numbers** - Use NNN (001, 002, etc.) for migrations on the same day
1988
+ 4. **Descriptive names** - Use clear descriptions (InitialSchema, AddUserProfiles, etc.)
1989
+ 5. **Schema must be specified** - All tables must specify their schema in ToTable()
1990
+
1991
+ ---
1992
+
1993
+ ## 3. Namespace Conventions
1994
+
1995
+ ### Layer Structure
1996
+
1997
+ | Layer | Namespace | Purpose |
1998
+ |-------|-----------|---------|
1999
+ | Domain | \`${namespaces.domain}\` | Entities, value objects, domain events |
2000
+ | Application | \`${namespaces.application}\` | Use cases, services, interfaces |
2001
+ | Infrastructure | \`${namespaces.infrastructure}\` | EF Core, external services |
2002
+ | API | \`${namespaces.api}\` | Controllers, DTOs, middleware |
2003
+
2004
+ ### Example Namespaces
2005
+
2006
+ \`\`\`csharp
2007
+ // Entity in Domain layer
2008
+ namespace ${namespaces.domain}.Entities;
2009
+ public class User { }
2010
+
2011
+ // Service interface in Application layer
2012
+ namespace ${namespaces.application}.Services;
2013
+ public interface IUserService { }
2014
+
2015
+ // EF Configuration in Infrastructure layer
2016
+ namespace ${namespaces.infrastructure}.Persistence.Configurations;
2017
+ public class UserConfiguration { }
2018
+
2019
+ // Controller in API layer
2020
+ namespace ${namespaces.api}.Controllers;
2021
+ public class UsersController { }
2022
+ \`\`\`
2023
+
2024
+ ---
2025
+
2026
+ ## 4. Service Conventions
2027
+
2028
+ ### Naming Pattern
2029
+
2030
+ | Type | Pattern | Example |
2031
+ |------|---------|---------|
2032
+ | Interface | \`${servicePattern.interface.replace("{Name}", "<Name>")}\` | \`IUserService\` |
2033
+ | Implementation | \`${servicePattern.implementation.replace("{Name}", "<Name>")}\` | \`UserService\` |
2034
+
2035
+ ### Service Structure
2036
+
2037
+ \`\`\`csharp
2038
+ // Interface (in Application layer)
2039
+ namespace ${namespaces.application}.Services;
2040
+
2041
+ public interface IUserService
2042
+ {
2043
+ Task<UserDto> GetByIdAsync(Guid id, CancellationToken ct = default);
2044
+ Task<IEnumerable<UserDto>> GetAllAsync(CancellationToken ct = default);
2045
+ Task<UserDto> CreateAsync(CreateUserCommand cmd, CancellationToken ct = default);
2046
+ Task UpdateAsync(Guid id, UpdateUserCommand cmd, CancellationToken ct = default);
2047
+ Task DeleteAsync(Guid id, CancellationToken ct = default);
2048
+ }
2049
+
2050
+ // Implementation (in Infrastructure or Application layer)
2051
+ public class UserService : IUserService
2052
+ {
2053
+ private readonly IRepository<User> _repository;
2054
+ private readonly ILogger<UserService> _logger;
2055
+
2056
+ public UserService(IRepository<User> repository, ILogger<UserService> logger)
2057
+ {
2058
+ _repository = repository;
2059
+ _logger = logger;
2060
+ }
2061
+
2062
+ // Implementation...
2063
+ }
2064
+ \`\`\`
2065
+
2066
+ ### Dependency Injection
2067
+
2068
+ \`\`\`csharp
2069
+ // In DependencyInjection.cs or ServiceCollectionExtensions.cs
2070
+ public static IServiceCollection AddApplicationServices(this IServiceCollection services)
2071
+ {
2072
+ services.AddScoped<IUserService, UserService>();
2073
+ services.AddScoped<IRoleService, RoleService>();
2074
+ // ...
2075
+ return services;
2076
+ }
2077
+ \`\`\`
2078
+
2079
+ ---
2080
+
2081
+ ## 5. Extension Patterns
2082
+
2083
+ ### Extending Services
2084
+
2085
+ Use DI replacement to override core services:
2086
+
2087
+ \`\`\`csharp
2088
+ // Your custom implementation
2089
+ public class CustomUserService : UserService, IUserService
2090
+ {
2091
+ public CustomUserService(IRepository<User> repo, ILogger<CustomUserService> logger)
2092
+ : base(repo, logger) { }
2093
+
2094
+ public override async Task<UserDto> GetByIdAsync(Guid id, CancellationToken ct = default)
2095
+ {
2096
+ // Custom logic before
2097
+ var result = await base.GetByIdAsync(id, ct);
2098
+ // Custom logic after
2099
+ return result;
2100
+ }
2101
+ }
2102
+
2103
+ // Replace in DI
2104
+ services.Replace(ServiceDescriptor.Scoped<IUserService, CustomUserService>());
2105
+ \`\`\`
2106
+
2107
+ ### Extension Hooks
2108
+
2109
+ Core services expose hooks for common extension points:
2110
+
2111
+ \`\`\`csharp
2112
+ public interface IUserServiceHooks
2113
+ {
2114
+ Task OnUserCreatedAsync(User user, CancellationToken ct);
2115
+ Task OnUserUpdatedAsync(User user, CancellationToken ct);
2116
+ Task OnUserDeletedAsync(Guid userId, CancellationToken ct);
2117
+ }
2118
+ \`\`\`
2119
+
2120
+ ---
2121
+
2122
+ ## 6. Configuration
2123
+
2124
+ ### appsettings.json Structure
2125
+
2126
+ \`\`\`json
2127
+ {
2128
+ "AtlasHub": {
2129
+ "Licensing": {
2130
+ "Key": "XXXX-XXXX-XXXX-XXXX",
2131
+ "Validate": true
2132
+ },
2133
+ "Features": {
2134
+ "AI": true,
2135
+ "Support": true,
2136
+ "Workflows": true
2137
+ }
2138
+ },
2139
+ "Client": {
2140
+ "Custom": {
2141
+ // Client-specific configuration
2142
+ }
2143
+ }
2144
+ }
2145
+ \`\`\`
2146
+
2147
+ ---
2148
+
2149
+ ## Quick Reference
2150
+
2151
+ | Category | Convention | Example |
2152
+ |----------|------------|---------|
2153
+ | Platform schema | \`${schemas.platform}\` | \`.ToTable("auth_Users", "${schemas.platform}")\` |
2154
+ | Extensions schema | \`${schemas.extensions}\` | \`.ToTable("client_Custom", "${schemas.extensions}")\` |
2155
+ | Table prefixes | \`${tablePrefixes.slice(0, 5).join(", ")}\`, etc. | \`auth_Users\`, \`nav_Modules\` |
2156
+ | Migration | \`YYYYMMDD_NNN_Description\` | \`20260115_001_InitialCreate\` |
2157
+ | Interface | \`I<Name>Service\` | \`IUserService\` |
2158
+ | Implementation | \`<Name>Service\` | \`UserService\` |
2159
+ | Domain namespace | \`${namespaces.domain}\` | - |
2160
+ | API namespace | \`${namespaces.api}\` | - |
2161
+ `;
2162
+ }
2163
+
2164
+ // src/resources/project-info.ts
2165
+ import path9 from "path";
2166
+ var projectInfoResourceTemplate = {
2167
+ uri: "smartstack://project",
2168
+ name: "SmartStack Project Info",
2169
+ description: "Current SmartStack project information, structure, and configuration",
2170
+ mimeType: "text/markdown"
2171
+ };
2172
+ async function getProjectInfoResource(config) {
2173
+ const projectPath = config.smartstack.projectPath;
2174
+ const projectInfo = await detectProject(projectPath);
2175
+ const structure = await findSmartStackStructure(projectPath);
2176
+ const lines = [];
2177
+ lines.push("# SmartStack Project Information");
2178
+ lines.push("");
2179
+ lines.push("## Overview");
2180
+ lines.push("");
2181
+ lines.push(`| Property | Value |`);
2182
+ lines.push(`|----------|-------|`);
2183
+ lines.push(`| **Name** | ${projectInfo.name} |`);
2184
+ lines.push(`| **Version** | ${projectInfo.version} |`);
2185
+ lines.push(`| **Path** | \`${projectPath}\` |`);
2186
+ lines.push(`| **Git Repository** | ${projectInfo.isGitRepo ? "Yes" : "No"} |`);
2187
+ if (projectInfo.currentBranch) {
2188
+ lines.push(`| **Current Branch** | \`${projectInfo.currentBranch}\` |`);
2189
+ }
2190
+ lines.push(`| **.NET Project** | ${projectInfo.hasDotNet ? "Yes" : "No"} |`);
2191
+ lines.push(`| **EF Core** | ${projectInfo.hasEfCore ? "Yes" : "No"} |`);
2192
+ if (projectInfo.dbContextName) {
2193
+ lines.push(`| **DbContext** | \`${projectInfo.dbContextName}\` |`);
2194
+ }
2195
+ lines.push(`| **React Frontend** | ${projectInfo.hasReact ? "Yes" : "No"} |`);
2196
+ lines.push("");
2197
+ lines.push("## Project Structure");
2198
+ lines.push("");
2199
+ lines.push("```");
2200
+ lines.push(`${projectInfo.name}/`);
2201
+ if (structure.domain) {
2202
+ lines.push(`\u251C\u2500\u2500 ${path9.basename(structure.domain)}/ # Domain layer (entities)`);
2203
+ }
2204
+ if (structure.application) {
2205
+ lines.push(`\u251C\u2500\u2500 ${path9.basename(structure.application)}/ # Application layer (services)`);
2206
+ }
2207
+ if (structure.infrastructure) {
2208
+ lines.push(`\u251C\u2500\u2500 ${path9.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
2209
+ }
2210
+ if (structure.api) {
2211
+ lines.push(`\u251C\u2500\u2500 ${path9.basename(structure.api)}/ # API layer (controllers)`);
2212
+ }
2213
+ if (structure.web) {
2214
+ lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
2215
+ }
2216
+ lines.push("```");
2217
+ lines.push("");
2218
+ if (projectInfo.csprojFiles.length > 0) {
2219
+ lines.push("## .NET Projects");
2220
+ lines.push("");
2221
+ lines.push("| Project | Path |");
2222
+ lines.push("|---------|------|");
2223
+ for (const csproj of projectInfo.csprojFiles) {
2224
+ const name = path9.basename(csproj, ".csproj");
2225
+ const relativePath = path9.relative(projectPath, csproj);
2226
+ lines.push(`| ${name} | \`${relativePath}\` |`);
2227
+ }
2228
+ lines.push("");
2229
+ }
2230
+ if (structure.migrations) {
2231
+ const migrationFiles = await findFiles("*.cs", {
2232
+ cwd: structure.migrations,
2233
+ ignore: ["*.Designer.cs"]
2234
+ });
2235
+ const migrations = migrationFiles.map((f) => path9.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
2236
+ lines.push("## EF Core Migrations");
2237
+ lines.push("");
2238
+ lines.push(`**Location**: \`${path9.relative(projectPath, structure.migrations)}\``);
2239
+ lines.push(`**Total Migrations**: ${migrations.length}`);
2240
+ lines.push("");
2241
+ if (migrations.length > 0) {
2242
+ lines.push("### Recent Migrations");
2243
+ lines.push("");
2244
+ const recent = migrations.slice(-5);
2245
+ for (const migration of recent) {
2246
+ lines.push(`- \`${migration.replace(".cs", "")}\``);
2247
+ }
2248
+ lines.push("");
2249
+ }
2250
+ }
2251
+ lines.push("## Configuration");
2252
+ lines.push("");
2253
+ lines.push("### MCP Server Config");
2254
+ lines.push("");
2255
+ lines.push("```json");
2256
+ lines.push(JSON.stringify({
2257
+ smartstack: config.smartstack,
2258
+ conventions: {
2259
+ tablePrefix: config.conventions.tablePrefix,
2260
+ migrationFormat: config.conventions.migrationFormat
2261
+ }
2262
+ }, null, 2));
2263
+ lines.push("```");
2264
+ lines.push("");
2265
+ lines.push("## Quick Commands");
2266
+ lines.push("");
2267
+ lines.push("```bash");
2268
+ lines.push("# Build the solution");
2269
+ lines.push("dotnet build");
2270
+ lines.push("");
2271
+ lines.push("# Run API");
2272
+ lines.push(`cd ${structure.api ? path9.relative(projectPath, structure.api) : "SmartStack.Api"}`);
2273
+ lines.push("dotnet run");
2274
+ lines.push("");
2275
+ lines.push("# Run frontend");
2276
+ lines.push(`cd ${structure.web ? path9.relative(projectPath, structure.web) : "web/smartstack-web"}`);
2277
+ lines.push("npm run dev");
2278
+ lines.push("");
2279
+ lines.push("# Create migration");
2280
+ lines.push("dotnet ef migrations add YYYYMMDD_Core_NNN_Description");
2281
+ lines.push("");
2282
+ lines.push("# Apply migrations");
2283
+ lines.push("dotnet ef database update");
2284
+ lines.push("```");
2285
+ lines.push("");
2286
+ lines.push("## Available MCP Tools");
2287
+ lines.push("");
2288
+ lines.push("| Tool | Description |");
2289
+ lines.push("|------|-------------|");
2290
+ lines.push("| `validate_conventions` | Check code against AtlasHub conventions |");
2291
+ lines.push("| `check_migrations` | Analyze EF Core migrations for conflicts |");
2292
+ lines.push("| `scaffold_extension` | Generate service, entity, controller, or component |");
2293
+ lines.push("| `api_docs` | Get API endpoint documentation |");
2294
+ lines.push("");
2295
+ return lines.join("\n");
2296
+ }
2297
+
2298
+ // src/resources/api-endpoints.ts
2299
+ import path10 from "path";
2300
+ var apiEndpointsResourceTemplate = {
2301
+ uri: "smartstack://api/",
2302
+ name: "SmartStack API Endpoints",
2303
+ description: "API endpoint documentation. Use smartstack://api/{endpoint} to filter.",
2304
+ mimeType: "text/markdown"
2305
+ };
2306
+ async function getApiEndpointsResource(config, endpointFilter) {
2307
+ const structure = await findSmartStackStructure(config.smartstack.projectPath);
2308
+ if (!structure.api) {
2309
+ return "# API Endpoints\n\nNo API project found.";
2310
+ }
2311
+ const controllerFiles = await findControllerFiles(structure.api);
2312
+ const allEndpoints = [];
2313
+ for (const file of controllerFiles) {
2314
+ const endpoints2 = await parseController(file, structure.root);
2315
+ allEndpoints.push(...endpoints2);
2316
+ }
2317
+ const endpoints = endpointFilter ? allEndpoints.filter(
2318
+ (e) => e.path.toLowerCase().includes(endpointFilter.toLowerCase()) || e.controller.toLowerCase().includes(endpointFilter.toLowerCase())
2319
+ ) : allEndpoints;
2320
+ return formatEndpoints(endpoints, endpointFilter);
2321
+ }
2322
+ async function parseController(filePath, rootPath) {
2323
+ const content = await readText(filePath);
2324
+ const fileName = path10.basename(filePath, ".cs");
2325
+ const controllerName = fileName.replace("Controller", "");
2326
+ const endpoints = [];
2327
+ const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
2328
+ const baseRoute = routeMatch ? routeMatch[1].replace("[controller]", controllerName.toLowerCase()) : `/api/${controllerName.toLowerCase()}`;
2329
+ const classAuthorize = /\[Authorize\s*(?:\([^)]*\))?\s*\]\s*(?:\[.*\]\s*)*public\s+class/.test(content);
2330
+ const methodPattern = /\/\/\/\s*<summary>\s*\n\s*\/\/\/\s*([^\n]+)\s*\n\s*\/\/\/\s*<\/summary>[\s\S]*?(?=\[Http)|(\[Http(Get|Post|Put|Patch|Delete)(?:\s*\(\s*"([^"]*)"\s*\))?\][\s\S]*?public\s+(?:async\s+)?(?:Task<)?(?:ActionResult<)?(\w+)(?:[<>[\],\s\w]*)?\s+(\w+)\s*\(([^)]*)\))/g;
2331
+ let lastSummary = "";
2332
+ let match;
2333
+ const httpMethods = ["HttpGet", "HttpPost", "HttpPut", "HttpPatch", "HttpDelete"];
2334
+ for (const httpMethod of httpMethods) {
2335
+ const regex = new RegExp(
2336
+ `\\[${httpMethod}(?:\\s*\\(\\s*"([^"]*)"\\s*\\))?\\]\\s*(?:\\[.*?\\]\\s*)*public\\s+(?:async\\s+)?(?:Task<)?(?:ActionResult<)?(\\w+)(?:[<>\\[\\],\\s\\w]*)?\\s+(\\w+)\\s*\\(([^)]*)\\)`,
2337
+ "g"
2338
+ );
2339
+ while ((match = regex.exec(content)) !== null) {
2340
+ const routeSuffix = match[1] || "";
2341
+ const returnType = match[2];
2342
+ const actionName = match[3];
2343
+ const params = match[4];
2344
+ let fullPath = baseRoute;
2345
+ if (routeSuffix) {
2346
+ fullPath = `${baseRoute}/${routeSuffix}`;
2347
+ }
2348
+ const parameters = parseParameters(params);
2349
+ const methodAuthorize = content.substring(Math.max(0, match.index - 200), match.index).includes("[Authorize");
2350
+ endpoints.push({
2351
+ method: httpMethod.replace("Http", "").toUpperCase(),
2352
+ path: fullPath.replace(/\/+/g, "/"),
2353
+ controller: controllerName,
2354
+ action: actionName,
2355
+ parameters,
2356
+ returnType,
2357
+ authorize: classAuthorize || methodAuthorize
2358
+ });
2359
+ }
2360
+ }
2361
+ return endpoints;
2362
+ }
2363
+ function parseParameters(paramsString) {
2364
+ if (!paramsString.trim()) return [];
2365
+ const params = [];
2366
+ const parts = paramsString.split(",");
2367
+ for (const part of parts) {
2368
+ const trimmed = part.trim();
2369
+ if (!trimmed) continue;
2370
+ const match = trimmed.match(/(\w+)\s*(?:=.*)?$/);
2371
+ if (match) {
2372
+ if (trimmed.includes("[FromBody]")) {
2373
+ params.push(`body: ${match[1]}`);
2374
+ } else if (trimmed.includes("[FromQuery]")) {
2375
+ params.push(`query: ${match[1]}`);
2376
+ } else if (trimmed.includes("[FromRoute]")) {
2377
+ params.push(`route: ${match[1]}`);
2378
+ } else {
2379
+ params.push(match[1]);
2380
+ }
2381
+ }
2382
+ }
2383
+ return params;
2384
+ }
2385
+ function formatEndpoints(endpoints, filter) {
2386
+ const lines = [];
2387
+ lines.push("# SmartStack API Endpoints");
2388
+ lines.push("");
2389
+ if (filter) {
2390
+ lines.push(`> Filtered by: \`${filter}\``);
2391
+ lines.push("");
2392
+ }
2393
+ if (endpoints.length === 0) {
2394
+ lines.push("No endpoints found matching the criteria.");
2395
+ return lines.join("\n");
2396
+ }
2397
+ const byController = /* @__PURE__ */ new Map();
2398
+ for (const endpoint of endpoints) {
2399
+ const existing = byController.get(endpoint.controller) || [];
2400
+ existing.push(endpoint);
2401
+ byController.set(endpoint.controller, existing);
2402
+ }
2403
+ lines.push("## Summary");
2404
+ lines.push("");
2405
+ lines.push(`**Total Endpoints**: ${endpoints.length}`);
2406
+ lines.push(`**Controllers**: ${byController.size}`);
2407
+ lines.push("");
2408
+ const methodCounts = /* @__PURE__ */ new Map();
2409
+ for (const endpoint of endpoints) {
2410
+ methodCounts.set(endpoint.method, (methodCounts.get(endpoint.method) || 0) + 1);
2411
+ }
2412
+ lines.push("| Method | Count |");
2413
+ lines.push("|--------|-------|");
2414
+ for (const [method, count] of methodCounts) {
2415
+ lines.push(`| ${method} | ${count} |`);
2416
+ }
2417
+ lines.push("");
2418
+ for (const [controller, controllerEndpoints] of byController) {
2419
+ lines.push(`## ${controller}Controller`);
2420
+ lines.push("");
2421
+ controllerEndpoints.sort((a, b) => {
2422
+ const pathCompare = a.path.localeCompare(b.path);
2423
+ if (pathCompare !== 0) return pathCompare;
2424
+ return a.method.localeCompare(b.method);
2425
+ });
2426
+ for (const endpoint of controllerEndpoints) {
2427
+ const authBadge = endpoint.authorize ? " \u{1F512}" : "";
2428
+ const methodColor = getMethodEmoji(endpoint.method);
2429
+ lines.push(`### ${methodColor} \`${endpoint.method}\` ${endpoint.path}${authBadge}`);
2430
+ lines.push("");
2431
+ lines.push(`**Action**: \`${endpoint.action}\``);
2432
+ lines.push(`**Returns**: \`${endpoint.returnType}\``);
2433
+ if (endpoint.parameters.length > 0) {
2434
+ lines.push("");
2435
+ lines.push("**Parameters**:");
2436
+ for (const param of endpoint.parameters) {
2437
+ lines.push(`- \`${param}\``);
2438
+ }
2439
+ }
2440
+ lines.push("");
2441
+ }
2442
+ }
2443
+ lines.push("## Quick Reference");
2444
+ lines.push("");
2445
+ lines.push("| Method | Path | Action | Auth |");
2446
+ lines.push("|--------|------|--------|------|");
2447
+ for (const endpoint of endpoints) {
2448
+ lines.push(
2449
+ `| ${endpoint.method} | \`${endpoint.path}\` | ${endpoint.action} | ${endpoint.authorize ? "\u{1F512}" : "\u2713"} |`
2450
+ );
2451
+ }
2452
+ lines.push("");
2453
+ return lines.join("\n");
2454
+ }
2455
+ function getMethodEmoji(method) {
2456
+ switch (method) {
2457
+ case "GET":
2458
+ return "\u{1F535}";
2459
+ case "POST":
2460
+ return "\u{1F7E2}";
2461
+ case "PUT":
2462
+ return "\u{1F7E1}";
2463
+ case "PATCH":
2464
+ return "\u{1F7E0}";
2465
+ case "DELETE":
2466
+ return "\u{1F534}";
2467
+ default:
2468
+ return "\u26AA";
2469
+ }
2470
+ }
2471
+
2472
+ // src/resources/db-schema.ts
2473
+ import path11 from "path";
2474
+ var dbSchemaResourceTemplate = {
2475
+ uri: "smartstack://schema/",
2476
+ name: "SmartStack Database Schema",
2477
+ description: "Database schema information. Use smartstack://schema/{table} to get specific table.",
2478
+ mimeType: "text/markdown"
2479
+ };
2480
+ async function getDbSchemaResource(config, tableFilter) {
2481
+ const structure = await findSmartStackStructure(config.smartstack.projectPath);
2482
+ if (!structure.domain && !structure.infrastructure) {
2483
+ return "# Database Schema\n\nNo domain or infrastructure project found.";
2484
+ }
2485
+ const entities = [];
2486
+ if (structure.domain) {
2487
+ const entityFiles = await findEntityFiles(structure.domain);
2488
+ for (const file of entityFiles) {
2489
+ const entity = await parseEntity(file, structure.root, config);
2490
+ if (entity) {
2491
+ entities.push(entity);
2492
+ }
2493
+ }
2494
+ }
2495
+ if (structure.infrastructure) {
2496
+ await enrichFromConfigurations(entities, structure.infrastructure, config);
2497
+ }
2498
+ const filteredEntities = tableFilter ? entities.filter(
2499
+ (e) => e.name.toLowerCase().includes(tableFilter.toLowerCase()) || e.tableName.toLowerCase().includes(tableFilter.toLowerCase())
2500
+ ) : entities;
2501
+ return formatSchema(filteredEntities, tableFilter, config);
2502
+ }
2503
+ async function parseEntity(filePath, rootPath, config) {
2504
+ const content = await readText(filePath);
2505
+ const fileName = path11.basename(filePath, ".cs");
2506
+ const classMatch = content.match(/public\s+(?:class|record)\s+(\w+)(?:\s*:\s*(\w+))?/);
2507
+ if (!classMatch) return null;
2508
+ const entityName = classMatch[1];
2509
+ const baseClass = classMatch[2];
2510
+ if (entityName.endsWith("Dto") || entityName.endsWith("Command") || entityName.endsWith("Query") || entityName.endsWith("Handler")) {
2511
+ return null;
2512
+ }
2513
+ const properties = [];
2514
+ const relationships = [];
2515
+ const propertyPattern = /public\s+(?:required\s+)?(\w+(?:<[\w,\s]+>)?)\??\s+(\w+)\s*\{/g;
2516
+ let match;
2517
+ while ((match = propertyPattern.exec(content)) !== null) {
2518
+ const propertyType = match[1];
2519
+ const propertyName = match[2];
2520
+ if (propertyType.startsWith("ICollection") || propertyType.startsWith("List")) {
2521
+ const targetMatch = propertyType.match(/<(\w+)>/);
2522
+ if (targetMatch) {
2523
+ relationships.push({
2524
+ type: "one-to-many",
2525
+ targetEntity: targetMatch[1],
2526
+ propertyName
2527
+ });
2528
+ }
2529
+ continue;
2530
+ }
2531
+ const isNavigationProperty = /^[A-Z]/.test(propertyType) && ![
2532
+ "Guid",
2533
+ "String",
2534
+ "Int32",
2535
+ "Int64",
2536
+ "DateTime",
2537
+ "DateTimeOffset",
2538
+ "Boolean",
2539
+ "Decimal",
2540
+ "Double",
2541
+ "Float",
2542
+ "Byte"
2543
+ ].includes(propertyType) && !propertyType.includes("?");
2544
+ if (isNavigationProperty && !propertyType.includes("<")) {
2545
+ relationships.push({
2546
+ type: "one-to-one",
2547
+ targetEntity: propertyType,
2548
+ propertyName
2549
+ });
2550
+ continue;
2551
+ }
2552
+ properties.push({
2553
+ name: propertyName,
2554
+ type: mapCSharpType(propertyType),
2555
+ nullable: content.includes(`${propertyType}? ${propertyName}`) || content.includes(`${propertyType}? ${propertyName}`),
2556
+ isPrimaryKey: propertyName === "Id" || propertyName === `${entityName}Id`
2557
+ });
2558
+ }
2559
+ const tableName = `${config.conventions.tablePrefix.core}${entityName}s`;
2560
+ return {
2561
+ name: entityName,
2562
+ tableName,
2563
+ properties,
2564
+ relationships,
2565
+ file: path11.relative(rootPath, filePath)
2566
+ };
2567
+ }
2568
+ async function enrichFromConfigurations(entities, infrastructurePath, config) {
2569
+ const configFiles = await findFiles("**/Configurations/**/*.cs", {
2570
+ cwd: infrastructurePath
2571
+ });
2572
+ for (const file of configFiles) {
2573
+ const content = await readText(file);
2574
+ const tableMatch = content.match(/\.ToTable\s*\(\s*"([^"]+)"/);
2575
+ if (tableMatch) {
2576
+ const entityMatch = content.match(/IEntityTypeConfiguration<(\w+)>/);
2577
+ if (entityMatch) {
2578
+ const entityName = entityMatch[1];
2579
+ const entity = entities.find((e) => e.name === entityName);
2580
+ if (entity) {
2581
+ entity.tableName = tableMatch[1];
2582
+ }
2583
+ }
2584
+ }
2585
+ const maxLengthMatches = content.matchAll(/\.Property\s*\(\s*\w+\s*=>\s*\w+\.(\w+)\s*\)[\s\S]*?\.HasMaxLength\s*\(\s*(\d+)\s*\)/g);
2586
+ for (const match of maxLengthMatches) {
2587
+ const propertyName = match[1];
2588
+ const maxLength = parseInt(match[2], 10);
2589
+ for (const entity of entities) {
2590
+ const property = entity.properties.find((p) => p.name === propertyName);
2591
+ if (property) {
2592
+ property.maxLength = maxLength;
2593
+ }
2594
+ }
2595
+ }
2596
+ }
2597
+ }
2598
+ function mapCSharpType(csharpType) {
2599
+ const typeMap = {
2600
+ "Guid": "uniqueidentifier",
2601
+ "string": "nvarchar",
2602
+ "String": "nvarchar",
2603
+ "int": "int",
2604
+ "Int32": "int",
2605
+ "long": "bigint",
2606
+ "Int64": "bigint",
2607
+ "DateTime": "datetime2",
2608
+ "DateTimeOffset": "datetimeoffset",
2609
+ "bool": "bit",
2610
+ "Boolean": "bit",
2611
+ "decimal": "decimal",
2612
+ "Decimal": "decimal",
2613
+ "double": "float",
2614
+ "Double": "float",
2615
+ "float": "real",
2616
+ "Float": "real",
2617
+ "byte[]": "varbinary"
2618
+ };
2619
+ const baseType = csharpType.replace("?", "");
2620
+ return typeMap[baseType] || "nvarchar";
2621
+ }
2622
+ function formatSchema(entities, filter, config) {
2623
+ const lines = [];
2624
+ lines.push("# SmartStack Database Schema");
2625
+ lines.push("");
2626
+ if (filter) {
2627
+ lines.push(`> Filtered by: \`${filter}\``);
2628
+ lines.push("");
2629
+ }
2630
+ if (entities.length === 0) {
2631
+ lines.push("No entities found matching the criteria.");
2632
+ return lines.join("\n");
2633
+ }
2634
+ lines.push("## Summary");
2635
+ lines.push("");
2636
+ lines.push(`**Total Entities**: ${entities.length}`);
2637
+ lines.push("");
2638
+ lines.push("| Entity | Table | Properties | Relationships |");
2639
+ lines.push("|--------|-------|------------|---------------|");
2640
+ for (const entity of entities) {
2641
+ lines.push(
2642
+ `| ${entity.name} | \`${entity.tableName}\` | ${entity.properties.length} | ${entity.relationships.length} |`
2643
+ );
2644
+ }
2645
+ lines.push("");
2646
+ for (const entity of entities) {
2647
+ lines.push(`## ${entity.name}`);
2648
+ lines.push("");
2649
+ lines.push(`**Table**: \`${entity.tableName}\``);
2650
+ lines.push(`**File**: \`${entity.file}\``);
2651
+ lines.push("");
2652
+ lines.push("### Columns");
2653
+ lines.push("");
2654
+ lines.push("| Column | SQL Type | Nullable | Notes |");
2655
+ lines.push("|--------|----------|----------|-------|");
2656
+ for (const prop of entity.properties) {
2657
+ const notes = [];
2658
+ if (prop.isPrimaryKey) notes.push("PK");
2659
+ if (prop.maxLength) notes.push(`MaxLength(${prop.maxLength})`);
2660
+ lines.push(
2661
+ `| ${prop.name} | ${prop.type} | ${prop.nullable ? "Yes" : "No"} | ${notes.join(", ")} |`
2662
+ );
2663
+ }
2664
+ lines.push("");
2665
+ if (entity.relationships.length > 0) {
2666
+ lines.push("### Relationships");
2667
+ lines.push("");
2668
+ lines.push("| Type | Target | Property |");
2669
+ lines.push("|------|--------|----------|");
2670
+ for (const rel of entity.relationships) {
2671
+ const typeIcon = rel.type === "one-to-one" ? "1:1" : rel.type === "one-to-many" ? "1:N" : "N:N";
2672
+ lines.push(`| ${typeIcon} | ${rel.targetEntity} | ${rel.propertyName} |`);
2673
+ }
2674
+ lines.push("");
2675
+ }
2676
+ lines.push("### EF Core Configuration");
2677
+ lines.push("");
2678
+ lines.push("```csharp");
2679
+ lines.push(`public class ${entity.name}Configuration : IEntityTypeConfiguration<${entity.name}>`);
2680
+ lines.push("{");
2681
+ lines.push(` public void Configure(EntityTypeBuilder<${entity.name}> builder)`);
2682
+ lines.push(" {");
2683
+ lines.push(` builder.ToTable("${entity.tableName}");`);
2684
+ lines.push("");
2685
+ const pk = entity.properties.find((p) => p.isPrimaryKey);
2686
+ if (pk) {
2687
+ lines.push(` builder.HasKey(e => e.${pk.name});`);
2688
+ }
2689
+ lines.push(" }");
2690
+ lines.push("}");
2691
+ lines.push("```");
2692
+ lines.push("");
2693
+ }
2694
+ if (entities.length > 1) {
2695
+ lines.push("## Entity Relationships");
2696
+ lines.push("");
2697
+ lines.push("```");
2698
+ for (const entity of entities) {
2699
+ for (const rel of entity.relationships) {
2700
+ const arrow = rel.type === "one-to-one" ? "\u2500\u2500" : rel.type === "one-to-many" ? "\u2500<" : ">\u2500<";
2701
+ lines.push(`${entity.name} ${arrow} ${rel.targetEntity}`);
2702
+ }
2703
+ }
2704
+ lines.push("```");
2705
+ lines.push("");
2706
+ }
2707
+ return lines.join("\n");
2708
+ }
2709
+
2710
+ // src/server.ts
2711
+ async function createServer() {
2712
+ const config = await getConfig();
2713
+ const server = new Server(
2714
+ {
2715
+ name: "smartstack-mcp",
2716
+ version: "1.0.0"
2717
+ },
2718
+ {
2719
+ capabilities: {
2720
+ tools: {},
2721
+ resources: {}
2722
+ }
2723
+ }
2724
+ );
2725
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
2726
+ logger.debug("Listing tools");
2727
+ return {
2728
+ tools: [
2729
+ validateConventionsTool,
2730
+ checkMigrationsTool,
2731
+ scaffoldExtensionTool,
2732
+ apiDocsTool
2733
+ ]
2734
+ };
2735
+ });
2736
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2737
+ const { name, arguments: args } = request.params;
2738
+ const startTime = Date.now();
2739
+ logger.toolStart(name, args);
2740
+ try {
2741
+ let result;
2742
+ switch (name) {
2743
+ case "validate_conventions":
2744
+ result = await handleValidateConventions(args, config);
2745
+ break;
2746
+ case "check_migrations":
2747
+ result = await handleCheckMigrations(args, config);
2748
+ break;
2749
+ case "scaffold_extension":
2750
+ result = await handleScaffoldExtension(args, config);
2751
+ break;
2752
+ case "api_docs":
2753
+ result = await handleApiDocs(args, config);
2754
+ break;
2755
+ default:
2756
+ throw new Error(`Unknown tool: ${name}`);
2757
+ }
2758
+ logger.toolEnd(name, true, Date.now() - startTime);
2759
+ return {
2760
+ content: [
2761
+ {
2762
+ type: "text",
2763
+ text: result
2764
+ }
2765
+ ]
2766
+ };
2767
+ } catch (error) {
2768
+ const err = error instanceof Error ? error : new Error(String(error));
2769
+ logger.toolError(name, err);
2770
+ return {
2771
+ content: [
2772
+ {
2773
+ type: "text",
2774
+ text: `Error: ${err.message}`
2775
+ }
2776
+ ],
2777
+ isError: true
2778
+ };
2779
+ }
2780
+ });
2781
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
2782
+ logger.debug("Listing resources");
2783
+ return {
2784
+ resources: [
2785
+ conventionsResourceTemplate,
2786
+ projectInfoResourceTemplate,
2787
+ apiEndpointsResourceTemplate,
2788
+ dbSchemaResourceTemplate
2789
+ ]
2790
+ };
2791
+ });
2792
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
2793
+ const { uri } = request.params;
2794
+ logger.debug("Reading resource", { uri });
2795
+ try {
2796
+ let content;
2797
+ let mimeType = "text/markdown";
2798
+ if (uri === "smartstack://conventions") {
2799
+ content = await getConventionsResource(config);
2800
+ } else if (uri === "smartstack://project") {
2801
+ content = await getProjectInfoResource(config);
2802
+ } else if (uri.startsWith("smartstack://api/")) {
2803
+ const endpoint = uri.replace("smartstack://api/", "");
2804
+ content = await getApiEndpointsResource(config, endpoint);
2805
+ } else if (uri.startsWith("smartstack://schema/")) {
2806
+ const table = uri.replace("smartstack://schema/", "");
2807
+ content = await getDbSchemaResource(config, table);
2808
+ } else {
2809
+ throw new Error(`Unknown resource: ${uri}`);
2810
+ }
2811
+ return {
2812
+ contents: [
2813
+ {
2814
+ uri,
2815
+ mimeType,
2816
+ text: content
2817
+ }
2818
+ ]
2819
+ };
2820
+ } catch (error) {
2821
+ const err = error instanceof Error ? error : new Error(String(error));
2822
+ logger.error("Resource read failed", { uri, error: err.message });
2823
+ throw err;
2824
+ }
2825
+ });
2826
+ return server;
2827
+ }
2828
+ async function runServer() {
2829
+ logger.info("Starting SmartStack MCP Server...");
2830
+ const server = await createServer();
2831
+ const transport = new StdioServerTransport();
2832
+ await server.connect(transport);
2833
+ logger.info("SmartStack MCP Server running on stdio");
2834
+ }
2835
+
2836
+ // src/index.ts
2837
+ process.on("uncaughtException", (error) => {
2838
+ logger.error("Uncaught exception", { error: error.message, stack: error.stack });
2839
+ process.exit(1);
2840
+ });
2841
+ process.on("unhandledRejection", (reason) => {
2842
+ logger.error("Unhandled rejection", { reason });
2843
+ process.exit(1);
2844
+ });
2845
+ runServer().catch((error) => {
2846
+ logger.error("Failed to start server", { error: error.message });
2847
+ process.exit(1);
2848
+ });
2849
+ //# sourceMappingURL=index.js.map