@atlashub/smartstack-mcp 1.5.1 → 1.6.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 CHANGED
@@ -82,10 +82,10 @@ import { stat, mkdir, readFile, writeFile, cp, rm } from "fs/promises";
82
82
  import path from "path";
83
83
  import { glob } from "glob";
84
84
  var FileSystemError = class extends Error {
85
- constructor(message, operation, path18, cause) {
85
+ constructor(message, operation, path21, cause) {
86
86
  super(message);
87
87
  this.operation = operation;
88
- this.path = path18;
88
+ this.path = path21;
89
89
  this.cause = cause;
90
90
  this.name = "FileSystemError";
91
91
  }
@@ -491,6 +491,35 @@ var SuggestTestScenariosInputSchema = z.object({
491
491
  name: z.string().min(1).describe("Component name or file path"),
492
492
  depth: z.enum(["basic", "comprehensive", "security-focused"]).default("comprehensive").describe("Depth of analysis")
493
493
  });
494
+ var ScaffoldApiClientInputSchema = z.object({
495
+ navRoute: z.string().min(1).describe('NavRoute path (e.g., "platform.administration.users")'),
496
+ name: z.string().min(1).describe('Entity name in PascalCase (e.g., "User", "Order")'),
497
+ methods: z.array(z.enum(["getAll", "getById", "create", "update", "delete", "search", "export"])).default(["getAll", "getById", "create", "update", "delete"]).describe("API methods to generate"),
498
+ options: z.object({
499
+ outputPath: z.string().optional().describe("Custom output path for generated files"),
500
+ includeTypes: z.boolean().default(true).describe("Generate TypeScript types"),
501
+ includeHook: z.boolean().default(true).describe("Generate React Query hook"),
502
+ dryRun: z.boolean().default(false).describe("Preview without writing files")
503
+ }).optional()
504
+ });
505
+ var ScaffoldRoutesInputSchema = z.object({
506
+ source: z.enum(["controllers", "navigation", "manual"]).default("controllers").describe("Source for route discovery: controllers (scan NavRoute attributes), navigation (from DB), manual (from config)"),
507
+ scope: z.enum(["all", "platform", "business", "extensions"]).default("all").describe("Scope of routes to generate"),
508
+ options: z.object({
509
+ outputPath: z.string().optional().describe("Custom output path"),
510
+ includeLayouts: z.boolean().default(true).describe("Generate layout components"),
511
+ includeGuards: z.boolean().default(true).describe("Include route guards for permissions"),
512
+ generateRegistry: z.boolean().default(true).describe("Generate navRoutes.generated.ts"),
513
+ dryRun: z.boolean().default(false).describe("Preview without writing files")
514
+ }).optional()
515
+ });
516
+ var ValidateFrontendRoutesInputSchema = z.object({
517
+ scope: z.enum(["api-clients", "routes", "registry", "all"]).default("all").describe("Scope of validation"),
518
+ options: z.object({
519
+ fix: z.boolean().default(false).describe("Auto-fix minor issues"),
520
+ strict: z.boolean().default(false).describe("Fail on warnings")
521
+ }).optional()
522
+ });
494
523
 
495
524
  // src/lib/detector.ts
496
525
  import path4 from "path";
@@ -6321,6 +6350,1253 @@ function getTypeEmoji(type) {
6321
6350
  }
6322
6351
  }
6323
6352
 
6353
+ // src/tools/scaffold-api-client.ts
6354
+ import path14 from "path";
6355
+ var scaffoldApiClientTool = {
6356
+ name: "scaffold_api_client",
6357
+ description: `Generate TypeScript API client with NavRoute integration.
6358
+
6359
+ Creates:
6360
+ - Type-safe API service with CRUD methods
6361
+ - TypeScript interfaces for request/response
6362
+ - React Query hook (optional)
6363
+ - Integration with navRoutes.generated.ts registry
6364
+
6365
+ Example:
6366
+ scaffold_api_client navRoute="platform.administration.users" name="User"
6367
+
6368
+ The generated client automatically resolves the API path from the NavRoute registry,
6369
+ ensuring frontend routes stay synchronized with backend NavRoute attributes.`,
6370
+ inputSchema: {
6371
+ type: "object",
6372
+ properties: {
6373
+ navRoute: {
6374
+ type: "string",
6375
+ description: 'NavRoute path (e.g., "platform.administration.users")'
6376
+ },
6377
+ name: {
6378
+ type: "string",
6379
+ description: 'Entity name in PascalCase (e.g., "User", "Order")'
6380
+ },
6381
+ methods: {
6382
+ type: "array",
6383
+ items: {
6384
+ type: "string",
6385
+ enum: ["getAll", "getById", "create", "update", "delete", "search", "export"]
6386
+ },
6387
+ default: ["getAll", "getById", "create", "update", "delete"],
6388
+ description: "API methods to generate"
6389
+ },
6390
+ options: {
6391
+ type: "object",
6392
+ properties: {
6393
+ outputPath: { type: "string", description: "Custom output path" },
6394
+ includeTypes: { type: "boolean", default: true, description: "Generate TypeScript types" },
6395
+ includeHook: { type: "boolean", default: true, description: "Generate React Query hook" },
6396
+ dryRun: { type: "boolean", default: false, description: "Preview without writing" }
6397
+ }
6398
+ }
6399
+ },
6400
+ required: ["navRoute", "name"]
6401
+ }
6402
+ };
6403
+ async function handleScaffoldApiClient(args, config) {
6404
+ const input = ScaffoldApiClientInputSchema.parse(args);
6405
+ logger.info("Scaffolding API client", { navRoute: input.navRoute, name: input.name });
6406
+ const result = await scaffoldApiClient(input, config);
6407
+ return formatResult4(result, input);
6408
+ }
6409
+ async function scaffoldApiClient(input, config) {
6410
+ const result = {
6411
+ success: true,
6412
+ files: [],
6413
+ instructions: []
6414
+ };
6415
+ const { navRoute, name, methods, options } = input;
6416
+ const dryRun = options?.dryRun ?? false;
6417
+ const includeTypes = options?.includeTypes ?? true;
6418
+ const includeHook = options?.includeHook ?? true;
6419
+ const nameLower = name.charAt(0).toLowerCase() + name.slice(1);
6420
+ const apiPath = navRouteToApiPath(navRoute);
6421
+ const projectRoot = config.smartstack.projectPath;
6422
+ const structure = await findSmartStackStructure(projectRoot);
6423
+ const webPath = structure.web || path14.join(projectRoot, "web", "smartstack-web");
6424
+ const servicesPath = options?.outputPath || path14.join(webPath, "src", "services", "api");
6425
+ const hooksPath = path14.join(webPath, "src", "hooks");
6426
+ const typesPath = path14.join(webPath, "src", "types");
6427
+ const apiClientContent = generateApiClient(name, nameLower, navRoute, apiPath, methods);
6428
+ const apiClientFile = path14.join(servicesPath, `${nameLower}.ts`);
6429
+ if (!dryRun) {
6430
+ await ensureDirectory(servicesPath);
6431
+ await writeText(apiClientFile, apiClientContent);
6432
+ }
6433
+ result.files.push({ path: apiClientFile, content: apiClientContent, type: "created" });
6434
+ if (includeTypes) {
6435
+ const typesContent = generateTypes(name);
6436
+ const typesFile = path14.join(typesPath, `${nameLower}.ts`);
6437
+ if (!dryRun) {
6438
+ await ensureDirectory(typesPath);
6439
+ await writeText(typesFile, typesContent);
6440
+ }
6441
+ result.files.push({ path: typesFile, content: typesContent, type: "created" });
6442
+ }
6443
+ if (includeHook) {
6444
+ const hookContent = generateHook(name, nameLower, methods);
6445
+ const hookFile = path14.join(hooksPath, `use${name}.ts`);
6446
+ if (!dryRun) {
6447
+ await ensureDirectory(hooksPath);
6448
+ await writeText(hookFile, hookContent);
6449
+ }
6450
+ result.files.push({ path: hookFile, content: hookContent, type: "created" });
6451
+ }
6452
+ result.instructions.push(`Import the API client: import { ${nameLower}Api } from './services/api/${nameLower}';`);
6453
+ if (includeHook) {
6454
+ result.instructions.push(`Import the hook: import { use${name}, use${name}List } from './hooks/use${name}';`);
6455
+ }
6456
+ result.instructions.push(`Ensure navRoutes.generated.ts includes route: "${navRoute}"`);
6457
+ return result;
6458
+ }
6459
+ function navRouteToApiPath(navRoute) {
6460
+ return `/api/${navRoute.replace(/\./g, "/")}`;
6461
+ }
6462
+ function generateApiClient(name, nameLower, navRoute, apiPath, methods) {
6463
+ const template = `/**
6464
+ * ${name} API Client
6465
+ *
6466
+ * Auto-generated by SmartStack MCP - DO NOT EDIT MANUALLY
6467
+ * NavRoute: ${navRoute}
6468
+ * API Path: ${apiPath}
6469
+ */
6470
+
6471
+ import { getRoute } from '../routes/navRoutes.generated';
6472
+ import { apiClient } from '../lib/apiClient';
6473
+ import type {
6474
+ ${name},
6475
+ ${name}CreateRequest,
6476
+ ${name}UpdateRequest,
6477
+ ${name}ListResponse,
6478
+ PaginatedRequest,
6479
+ PaginatedResponse
6480
+ } from '../types/${nameLower}';
6481
+
6482
+ const ROUTE = getRoute('${navRoute}');
6483
+
6484
+ export const ${nameLower}Api = {
6485
+ ${methods.includes("getAll") ? ` /**
6486
+ * Get all ${name}s with pagination
6487
+ */
6488
+ async getAll(params?: PaginatedRequest): Promise<PaginatedResponse<${name}>> {
6489
+ const response = await apiClient.get<${name}ListResponse>(ROUTE.api, { params });
6490
+ return response.data;
6491
+ },
6492
+ ` : ""}
6493
+ ${methods.includes("getById") ? ` /**
6494
+ * Get ${name} by ID
6495
+ */
6496
+ async getById(id: string): Promise<${name}> {
6497
+ const response = await apiClient.get<${name}>(\`\${ROUTE.api}/\${id}\`);
6498
+ return response.data;
6499
+ },
6500
+ ` : ""}
6501
+ ${methods.includes("create") ? ` /**
6502
+ * Create new ${name}
6503
+ */
6504
+ async create(data: ${name}CreateRequest): Promise<${name}> {
6505
+ const response = await apiClient.post<${name}>(ROUTE.api, data);
6506
+ return response.data;
6507
+ },
6508
+ ` : ""}
6509
+ ${methods.includes("update") ? ` /**
6510
+ * Update existing ${name}
6511
+ */
6512
+ async update(id: string, data: ${name}UpdateRequest): Promise<${name}> {
6513
+ const response = await apiClient.put<${name}>(\`\${ROUTE.api}/\${id}\`, data);
6514
+ return response.data;
6515
+ },
6516
+ ` : ""}
6517
+ ${methods.includes("delete") ? ` /**
6518
+ * Delete ${name}
6519
+ */
6520
+ async delete(id: string): Promise<void> {
6521
+ await apiClient.delete(\`\${ROUTE.api}/\${id}\`);
6522
+ },
6523
+ ` : ""}
6524
+ ${methods.includes("search") ? ` /**
6525
+ * Search ${name}s
6526
+ */
6527
+ async search(query: string, params?: PaginatedRequest): Promise<PaginatedResponse<${name}>> {
6528
+ const response = await apiClient.get<${name}ListResponse>(\`\${ROUTE.api}/search\`, {
6529
+ params: { q: query, ...params }
6530
+ });
6531
+ return response.data;
6532
+ },
6533
+ ` : ""}
6534
+ ${methods.includes("export") ? ` /**
6535
+ * Export ${name}s to file
6536
+ */
6537
+ async export(format: 'csv' | 'xlsx' | 'pdf' = 'xlsx'): Promise<Blob> {
6538
+ const response = await apiClient.get(\`\${ROUTE.api}/export\`, {
6539
+ params: { format },
6540
+ responseType: 'blob'
6541
+ });
6542
+ return response.data;
6543
+ },
6544
+ ` : ""}
6545
+ /**
6546
+ * Get the NavRoute for this API
6547
+ */
6548
+ getRoute() {
6549
+ return ROUTE;
6550
+ },
6551
+ };
6552
+
6553
+ export default ${nameLower}Api;
6554
+ `;
6555
+ return template;
6556
+ }
6557
+ function generateTypes(name) {
6558
+ return `/**
6559
+ * ${name} Types
6560
+ *
6561
+ * Auto-generated by SmartStack MCP - Customize as needed
6562
+ */
6563
+
6564
+ export interface ${name} {
6565
+ id: string;
6566
+ code: string;
6567
+ name?: string;
6568
+ description?: string;
6569
+ isActive: boolean;
6570
+ createdAt: string;
6571
+ createdBy: string;
6572
+ updatedAt?: string;
6573
+ updatedBy?: string;
6574
+ }
6575
+
6576
+ export interface ${name}CreateRequest {
6577
+ code: string;
6578
+ name?: string;
6579
+ description?: string;
6580
+ }
6581
+
6582
+ export interface ${name}UpdateRequest {
6583
+ code?: string;
6584
+ name?: string;
6585
+ description?: string;
6586
+ isActive?: boolean;
6587
+ }
6588
+
6589
+ export interface ${name}ListResponse {
6590
+ items: ${name}[];
6591
+ totalCount: number;
6592
+ pageSize: number;
6593
+ currentPage: number;
6594
+ totalPages: number;
6595
+ }
6596
+
6597
+ export interface PaginatedRequest {
6598
+ page?: number;
6599
+ pageSize?: number;
6600
+ sortBy?: string;
6601
+ sortDirection?: 'asc' | 'desc';
6602
+ filter?: string;
6603
+ }
6604
+
6605
+ export interface PaginatedResponse<T> {
6606
+ items: T[];
6607
+ totalCount: number;
6608
+ pageSize: number;
6609
+ currentPage: number;
6610
+ totalPages: number;
6611
+ }
6612
+ `;
6613
+ }
6614
+ function generateHook(name, nameLower, methods) {
6615
+ return `/**
6616
+ * ${name} React Query Hooks
6617
+ *
6618
+ * Auto-generated by SmartStack MCP - DO NOT EDIT MANUALLY
6619
+ */
6620
+
6621
+ import { useQuery, useMutation, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
6622
+ import { ${nameLower}Api } from '../services/api/${nameLower}';
6623
+ import type { ${name}, ${name}CreateRequest, ${name}UpdateRequest, PaginatedRequest, PaginatedResponse } from '../types/${nameLower}';
6624
+
6625
+ const QUERY_KEY = '${nameLower}s';
6626
+
6627
+ ${methods.includes("getAll") ? `/**
6628
+ * Hook to fetch paginated ${name} list
6629
+ */
6630
+ export function use${name}List(
6631
+ params?: PaginatedRequest,
6632
+ options?: Omit<UseQueryOptions<PaginatedResponse<${name}>>, 'queryKey' | 'queryFn'>
6633
+ ) {
6634
+ return useQuery({
6635
+ queryKey: [QUERY_KEY, 'list', params],
6636
+ queryFn: () => ${nameLower}Api.getAll(params),
6637
+ ...options,
6638
+ });
6639
+ }
6640
+ ` : ""}
6641
+ ${methods.includes("getById") ? `/**
6642
+ * Hook to fetch single ${name} by ID
6643
+ */
6644
+ export function use${name}(
6645
+ id: string | undefined,
6646
+ options?: Omit<UseQueryOptions<${name}>, 'queryKey' | 'queryFn'>
6647
+ ) {
6648
+ return useQuery({
6649
+ queryKey: [QUERY_KEY, 'detail', id],
6650
+ queryFn: () => ${nameLower}Api.getById(id!),
6651
+ enabled: !!id,
6652
+ ...options,
6653
+ });
6654
+ }
6655
+ ` : ""}
6656
+ ${methods.includes("create") ? `/**
6657
+ * Hook to create new ${name}
6658
+ */
6659
+ export function use${name}Create(
6660
+ options?: UseMutationOptions<${name}, Error, ${name}CreateRequest>
6661
+ ) {
6662
+ const queryClient = useQueryClient();
6663
+
6664
+ return useMutation({
6665
+ mutationFn: (data: ${name}CreateRequest) => ${nameLower}Api.create(data),
6666
+ onSuccess: () => {
6667
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
6668
+ },
6669
+ ...options,
6670
+ });
6671
+ }
6672
+ ` : ""}
6673
+ ${methods.includes("update") ? `/**
6674
+ * Hook to update existing ${name}
6675
+ */
6676
+ export function use${name}Update(
6677
+ options?: UseMutationOptions<${name}, Error, { id: string; data: ${name}UpdateRequest }>
6678
+ ) {
6679
+ const queryClient = useQueryClient();
6680
+
6681
+ return useMutation({
6682
+ mutationFn: ({ id, data }) => ${nameLower}Api.update(id, data),
6683
+ onSuccess: (_, { id }) => {
6684
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
6685
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEY, 'detail', id] });
6686
+ },
6687
+ ...options,
6688
+ });
6689
+ }
6690
+ ` : ""}
6691
+ ${methods.includes("delete") ? `/**
6692
+ * Hook to delete ${name}
6693
+ */
6694
+ export function use${name}Delete(
6695
+ options?: UseMutationOptions<void, Error, string>
6696
+ ) {
6697
+ const queryClient = useQueryClient();
6698
+
6699
+ return useMutation({
6700
+ mutationFn: (id: string) => ${nameLower}Api.delete(id),
6701
+ onSuccess: () => {
6702
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
6703
+ },
6704
+ ...options,
6705
+ });
6706
+ }
6707
+ ` : ""}
6708
+ ${methods.includes("search") ? `/**
6709
+ * Hook to search ${name}s
6710
+ */
6711
+ export function use${name}Search(
6712
+ query: string,
6713
+ params?: PaginatedRequest,
6714
+ options?: Omit<UseQueryOptions<PaginatedResponse<${name}>>, 'queryKey' | 'queryFn'>
6715
+ ) {
6716
+ return useQuery({
6717
+ queryKey: [QUERY_KEY, 'search', query, params],
6718
+ queryFn: () => ${nameLower}Api.search(query, params),
6719
+ enabled: query.length >= 2,
6720
+ ...options,
6721
+ });
6722
+ }
6723
+ ` : ""}
6724
+ `;
6725
+ }
6726
+ function formatResult4(result, input) {
6727
+ const lines = [];
6728
+ lines.push(`# Scaffold API Client: ${input.name}`);
6729
+ lines.push("");
6730
+ if (input.options?.dryRun) {
6731
+ lines.push("> **DRY RUN** - No files were written");
6732
+ lines.push("");
6733
+ }
6734
+ lines.push(`## NavRoute Integration`);
6735
+ lines.push("");
6736
+ lines.push(`- **NavRoute**: \`${input.navRoute}\``);
6737
+ lines.push(`- **API Path**: \`${navRouteToApiPath(input.navRoute)}\``);
6738
+ lines.push(`- **Methods**: ${input.methods.join(", ")}`);
6739
+ lines.push("");
6740
+ lines.push("## Generated Files");
6741
+ lines.push("");
6742
+ for (const file of result.files) {
6743
+ const relativePath = file.path.replace(/\\/g, "/").split("/src/").pop() || file.path;
6744
+ lines.push(`### ${relativePath}`);
6745
+ lines.push("");
6746
+ lines.push("```typescript");
6747
+ lines.push(file.content.substring(0, 1500) + (file.content.length > 1500 ? "\n// ... (truncated)" : ""));
6748
+ lines.push("```");
6749
+ lines.push("");
6750
+ }
6751
+ lines.push("## Next Steps");
6752
+ lines.push("");
6753
+ for (const instruction of result.instructions) {
6754
+ lines.push(`- ${instruction}`);
6755
+ }
6756
+ lines.push("");
6757
+ lines.push("## Required Setup");
6758
+ lines.push("");
6759
+ lines.push("1. Ensure `navRoutes.generated.ts` exists (run `scaffold_routes`)");
6760
+ lines.push("2. Configure `apiClient` with base URL and auth interceptors");
6761
+ lines.push("3. Install dependencies: `npm install @tanstack/react-query axios`");
6762
+ return lines.join("\n");
6763
+ }
6764
+
6765
+ // src/tools/scaffold-routes.ts
6766
+ import path15 from "path";
6767
+ import { glob as glob2 } from "glob";
6768
+ var scaffoldRoutesTool = {
6769
+ name: "scaffold_routes",
6770
+ description: `Generate React Router configuration from backend NavRoute attributes.
6771
+
6772
+ Creates:
6773
+ - navRoutes.generated.ts: Registry of all routes with API paths and permissions
6774
+ - routes.tsx: React Router configuration with nested routes
6775
+ - Layout components (optional)
6776
+
6777
+ Example:
6778
+ scaffold_routes source="controllers" scope="all"
6779
+
6780
+ Scans backend controllers for [NavRoute("context.application.module")] attributes
6781
+ and generates corresponding frontend routing infrastructure.`,
6782
+ inputSchema: {
6783
+ type: "object",
6784
+ properties: {
6785
+ source: {
6786
+ type: "string",
6787
+ enum: ["controllers", "navigation", "manual"],
6788
+ default: "controllers",
6789
+ description: "Source for route discovery"
6790
+ },
6791
+ scope: {
6792
+ type: "string",
6793
+ enum: ["all", "platform", "business", "extensions"],
6794
+ default: "all",
6795
+ description: "Scope of routes to generate"
6796
+ },
6797
+ options: {
6798
+ type: "object",
6799
+ properties: {
6800
+ outputPath: { type: "string", description: "Custom output path" },
6801
+ includeLayouts: { type: "boolean", default: true },
6802
+ includeGuards: { type: "boolean", default: true },
6803
+ generateRegistry: { type: "boolean", default: true },
6804
+ dryRun: { type: "boolean", default: false }
6805
+ }
6806
+ }
6807
+ }
6808
+ }
6809
+ };
6810
+ async function handleScaffoldRoutes(args, config) {
6811
+ const input = ScaffoldRoutesInputSchema.parse(args);
6812
+ logger.info("Scaffolding routes", { source: input.source, scope: input.scope });
6813
+ const result = await scaffoldRoutes(input, config);
6814
+ return formatResult5(result, input);
6815
+ }
6816
+ async function scaffoldRoutes(input, config) {
6817
+ const result = {
6818
+ success: true,
6819
+ files: [],
6820
+ instructions: []
6821
+ };
6822
+ const { source, scope, options } = input;
6823
+ const dryRun = options?.dryRun ?? false;
6824
+ const includeLayouts = options?.includeLayouts ?? true;
6825
+ const includeGuards = options?.includeGuards ?? true;
6826
+ const generateRegistry = options?.generateRegistry ?? true;
6827
+ const projectRoot = config.smartstack.projectPath;
6828
+ const structure = await findSmartStackStructure(projectRoot);
6829
+ const webPath = structure.web || path15.join(projectRoot, "web", "smartstack-web");
6830
+ const routesPath = options?.outputPath || path15.join(webPath, "src", "routes");
6831
+ const navRoutes = await discoverNavRoutes(structure, scope);
6832
+ if (navRoutes.length === 0) {
6833
+ result.success = false;
6834
+ result.instructions.push("No NavRoute attributes found in controllers");
6835
+ return result;
6836
+ }
6837
+ if (generateRegistry) {
6838
+ const registryContent = generateNavRouteRegistry(navRoutes);
6839
+ const registryFile = path15.join(routesPath, "navRoutes.generated.ts");
6840
+ if (!dryRun) {
6841
+ await ensureDirectory(routesPath);
6842
+ await writeText(registryFile, registryContent);
6843
+ }
6844
+ result.files.push({ path: registryFile, content: registryContent, type: "created" });
6845
+ }
6846
+ const routerContent = generateRouterConfig(navRoutes, includeGuards);
6847
+ const routerFile = path15.join(routesPath, "index.tsx");
6848
+ if (!dryRun) {
6849
+ await ensureDirectory(routesPath);
6850
+ await writeText(routerFile, routerContent);
6851
+ }
6852
+ result.files.push({ path: routerFile, content: routerContent, type: "created" });
6853
+ if (includeLayouts) {
6854
+ const layoutsPath = path15.join(webPath, "src", "layouts");
6855
+ const contexts = [...new Set(navRoutes.map((r) => r.navRoute.split(".")[0]))];
6856
+ for (const context of contexts) {
6857
+ const layoutContent = generateLayout(context);
6858
+ const layoutFile = path15.join(layoutsPath, `${capitalize(context)}Layout.tsx`);
6859
+ if (!dryRun) {
6860
+ await ensureDirectory(layoutsPath);
6861
+ await writeText(layoutFile, layoutContent);
6862
+ }
6863
+ result.files.push({ path: layoutFile, content: layoutContent, type: "created" });
6864
+ }
6865
+ }
6866
+ if (includeGuards) {
6867
+ const guardsContent = generateRouteGuards();
6868
+ const guardsFile = path15.join(routesPath, "guards.tsx");
6869
+ if (!dryRun) {
6870
+ await writeText(guardsFile, guardsContent);
6871
+ }
6872
+ result.files.push({ path: guardsFile, content: guardsContent, type: "created" });
6873
+ }
6874
+ result.instructions.push(`Generated ${navRoutes.length} routes from ${source}`);
6875
+ result.instructions.push('Import routes: import { router } from "./routes";');
6876
+ result.instructions.push("Use with RouterProvider: <RouterProvider router={router} />");
6877
+ return result;
6878
+ }
6879
+ async function discoverNavRoutes(structure, scope) {
6880
+ const routes = [];
6881
+ const apiPath = structure.api;
6882
+ if (!apiPath) {
6883
+ logger.warn("No API project found");
6884
+ return routes;
6885
+ }
6886
+ const controllerFiles = await glob2("**/*Controller.cs", {
6887
+ cwd: apiPath,
6888
+ absolute: true,
6889
+ ignore: ["**/obj/**", "**/bin/**"]
6890
+ });
6891
+ for (const file of controllerFiles) {
6892
+ try {
6893
+ const content = await readText(file);
6894
+ const navRouteMatch = content.match(/\[NavRoute\s*\(\s*"([^"]+)"(?:\s*,\s*Suffix\s*=\s*"([^"]+)")?\s*\)\]/);
6895
+ if (navRouteMatch) {
6896
+ const navRoute = navRouteMatch[1];
6897
+ const suffix = navRouteMatch[2];
6898
+ const context = navRoute.split(".")[0];
6899
+ if (scope !== "all" && context !== scope) {
6900
+ continue;
6901
+ }
6902
+ const controllerMatch = path15.basename(file).match(/(.+)Controller\.cs$/);
6903
+ const controllerName = controllerMatch ? controllerMatch[1] : "Unknown";
6904
+ const methods = [];
6905
+ if (content.includes("[HttpGet]")) methods.push("GET");
6906
+ if (content.includes("[HttpPost]")) methods.push("POST");
6907
+ if (content.includes("[HttpPut]")) methods.push("PUT");
6908
+ if (content.includes("[HttpDelete]")) methods.push("DELETE");
6909
+ if (content.includes("[HttpPatch]")) methods.push("PATCH");
6910
+ const permissions = [];
6911
+ const authorizeMatches = content.matchAll(/\[Authorize\s*\(\s*[^)]*Policy\s*=\s*"([^"]+)"/g);
6912
+ for (const match of authorizeMatches) {
6913
+ permissions.push(match[1]);
6914
+ }
6915
+ const fullNavRoute = suffix ? `${navRoute}.${suffix}` : navRoute;
6916
+ routes.push({
6917
+ navRoute: fullNavRoute,
6918
+ apiPath: `/api/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
6919
+ webPath: `/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
6920
+ permissions,
6921
+ controller: controllerName,
6922
+ methods
6923
+ });
6924
+ }
6925
+ } catch {
6926
+ logger.debug(`Failed to parse controller: ${file}`);
6927
+ }
6928
+ }
6929
+ return routes.sort((a, b) => a.navRoute.localeCompare(b.navRoute));
6930
+ }
6931
+ function generateNavRouteRegistry(routes) {
6932
+ const lines = [
6933
+ "/**",
6934
+ " * NavRoute Registry",
6935
+ " *",
6936
+ " * Auto-generated by SmartStack MCP - DO NOT EDIT MANUALLY",
6937
+ " * Run `scaffold_routes` to regenerate",
6938
+ " */",
6939
+ "",
6940
+ "export interface NavRoute {",
6941
+ " navRoute: string;",
6942
+ " api: string;",
6943
+ " web: string;",
6944
+ " permissions: string[];",
6945
+ " controller?: string;",
6946
+ " methods: string[];",
6947
+ "}",
6948
+ "",
6949
+ "export const ROUTES: Record<string, NavRoute> = {"
6950
+ ];
6951
+ for (const route of routes) {
6952
+ lines.push(` '${route.navRoute}': {`);
6953
+ lines.push(` navRoute: '${route.navRoute}',`);
6954
+ lines.push(` api: '${route.apiPath}',`);
6955
+ lines.push(` web: '${route.webPath}',`);
6956
+ lines.push(` permissions: [${route.permissions.map((p) => `'${p}'`).join(", ")}],`);
6957
+ if (route.controller) {
6958
+ lines.push(` controller: '${route.controller}',`);
6959
+ }
6960
+ lines.push(` methods: [${route.methods.map((m) => `'${m}'`).join(", ")}],`);
6961
+ lines.push(" },");
6962
+ }
6963
+ lines.push("};");
6964
+ lines.push("");
6965
+ lines.push("/**");
6966
+ lines.push(" * Get route configuration by NavRoute path");
6967
+ lines.push(" */");
6968
+ lines.push("export function getRoute(navRoute: string): NavRoute {");
6969
+ lines.push(" const route = ROUTES[navRoute];");
6970
+ lines.push(" if (!route) {");
6971
+ lines.push(" throw new Error(`Route not found: ${navRoute}`);");
6972
+ lines.push(" }");
6973
+ lines.push(" return route;");
6974
+ lines.push("}");
6975
+ lines.push("");
6976
+ lines.push("/**");
6977
+ lines.push(" * Check if user has permission for route");
6978
+ lines.push(" */");
6979
+ lines.push("export function hasRoutePermission(navRoute: string, userPermissions: string[]): boolean {");
6980
+ lines.push(" const route = ROUTES[navRoute];");
6981
+ lines.push(" if (!route || route.permissions.length === 0) return true;");
6982
+ lines.push(" return route.permissions.some(p => userPermissions.includes(p));");
6983
+ lines.push("}");
6984
+ lines.push("");
6985
+ lines.push("/**");
6986
+ lines.push(" * Get all routes for a context");
6987
+ lines.push(" */");
6988
+ lines.push("export function getRoutesByContext(context: string): NavRoute[] {");
6989
+ lines.push(" return Object.values(ROUTES).filter(r => r.navRoute.startsWith(`${context}.`));");
6990
+ lines.push("}");
6991
+ lines.push("");
6992
+ return lines.join("\n");
6993
+ }
6994
+ function generateRouterConfig(routes, includeGuards) {
6995
+ const routeTree = buildRouteTree(routes);
6996
+ const lines = [
6997
+ "/**",
6998
+ " * React Router Configuration",
6999
+ " *",
7000
+ " * Auto-generated by SmartStack MCP - DO NOT EDIT MANUALLY",
7001
+ " */",
7002
+ "",
7003
+ "import { createBrowserRouter, RouteObject } from 'react-router-dom';",
7004
+ "import { ROUTES } from './navRoutes.generated';"
7005
+ ];
7006
+ if (includeGuards) {
7007
+ lines.push("import { ProtectedRoute, PermissionGuard } from './guards';");
7008
+ }
7009
+ const contexts = Object.keys(routeTree);
7010
+ for (const context of contexts) {
7011
+ lines.push(`import { ${capitalize(context)}Layout } from '../layouts/${capitalize(context)}Layout';`);
7012
+ }
7013
+ lines.push("");
7014
+ lines.push("// Page imports - customize these paths");
7015
+ for (const route of routes) {
7016
+ const pageName = route.navRoute.split(".").map(capitalize).join("");
7017
+ lines.push(`// import { ${pageName}Page } from '../pages/${pageName}Page';`);
7018
+ }
7019
+ lines.push("");
7020
+ lines.push("const routes: RouteObject[] = [");
7021
+ for (const [context, applications] of Object.entries(routeTree)) {
7022
+ lines.push(" {");
7023
+ lines.push(` path: '${context}',`);
7024
+ lines.push(` element: <${capitalize(context)}Layout />,`);
7025
+ lines.push(" children: [");
7026
+ for (const [app, modules] of Object.entries(applications)) {
7027
+ lines.push(" {");
7028
+ lines.push(` path: '${app}',`);
7029
+ lines.push(" children: [");
7030
+ for (const route of modules) {
7031
+ const modulePath = route.navRoute.split(".").slice(2).join("/");
7032
+ const pageName = route.navRoute.split(".").map(capitalize).join("");
7033
+ if (includeGuards && route.permissions.length > 0) {
7034
+ lines.push(" {");
7035
+ lines.push(` path: '${modulePath || ""}',`);
7036
+ lines.push(` element: (`);
7037
+ lines.push(` <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}>`);
7038
+ lines.push(` {/* <${pageName}Page /> */}`);
7039
+ lines.push(` <div>TODO: ${pageName}Page</div>`);
7040
+ lines.push(` </PermissionGuard>`);
7041
+ lines.push(` ),`);
7042
+ lines.push(" },");
7043
+ } else {
7044
+ lines.push(" {");
7045
+ lines.push(` path: '${modulePath || ""}',`);
7046
+ lines.push(` element: <div>TODO: ${pageName}Page</div>,`);
7047
+ lines.push(" },");
7048
+ }
7049
+ }
7050
+ lines.push(" ],");
7051
+ lines.push(" },");
7052
+ }
7053
+ lines.push(" ],");
7054
+ lines.push(" },");
7055
+ }
7056
+ lines.push("];");
7057
+ lines.push("");
7058
+ lines.push("export const router = createBrowserRouter(routes);");
7059
+ lines.push("");
7060
+ lines.push("export default router;");
7061
+ lines.push("");
7062
+ return lines.join("\n");
7063
+ }
7064
+ function buildRouteTree(routes) {
7065
+ const tree = {};
7066
+ for (const route of routes) {
7067
+ const parts = route.navRoute.split(".");
7068
+ const context = parts[0];
7069
+ const app = parts[1] || "default";
7070
+ if (!tree[context]) {
7071
+ tree[context] = {};
7072
+ }
7073
+ if (!tree[context][app]) {
7074
+ tree[context][app] = [];
7075
+ }
7076
+ tree[context][app].push(route);
7077
+ }
7078
+ return tree;
7079
+ }
7080
+ function generateLayout(context) {
7081
+ const contextCapitalized = capitalize(context);
7082
+ return `/**
7083
+ * ${contextCapitalized} Layout
7084
+ *
7085
+ * Auto-generated by SmartStack MCP - Customize as needed
7086
+ */
7087
+
7088
+ import React from 'react';
7089
+ import { Outlet, Link, useLocation } from 'react-router-dom';
7090
+ import { ROUTES, getRoutesByContext } from '../routes/navRoutes.generated';
7091
+
7092
+ export const ${contextCapitalized}Layout: React.FC = () => {
7093
+ const location = useLocation();
7094
+ const contextRoutes = getRoutesByContext('${context}');
7095
+
7096
+ return (
7097
+ <div className="flex h-screen bg-gray-100">
7098
+ {/* Sidebar */}
7099
+ <aside className="w-64 bg-white shadow-sm">
7100
+ <div className="p-4 border-b">
7101
+ <h1 className="text-xl font-semibold text-gray-900">${contextCapitalized}</h1>
7102
+ </div>
7103
+ <nav className="p-4">
7104
+ <ul className="space-y-2">
7105
+ {contextRoutes.map((route) => (
7106
+ <li key={route.navRoute}>
7107
+ <Link
7108
+ to={route.web}
7109
+ className={\`block px-3 py-2 rounded-md \${
7110
+ location.pathname === route.web
7111
+ ? 'bg-blue-50 text-blue-700'
7112
+ : 'text-gray-700 hover:bg-gray-50'
7113
+ }\`}
7114
+ >
7115
+ {route.navRoute.split('.').pop()}
7116
+ </Link>
7117
+ </li>
7118
+ ))}
7119
+ </ul>
7120
+ </nav>
7121
+ </aside>
7122
+
7123
+ {/* Main content */}
7124
+ <main className="flex-1 overflow-auto">
7125
+ <div className="p-6">
7126
+ <Outlet />
7127
+ </div>
7128
+ </main>
7129
+ </div>
7130
+ );
7131
+ };
7132
+
7133
+ export default ${contextCapitalized}Layout;
7134
+ `;
7135
+ }
7136
+ function generateRouteGuards() {
7137
+ return `/**
7138
+ * Route Guards
7139
+ *
7140
+ * Auto-generated by SmartStack MCP - Customize as needed
7141
+ */
7142
+
7143
+ import React from 'react';
7144
+ import { Navigate, useLocation } from 'react-router-dom';
7145
+
7146
+ interface ProtectedRouteProps {
7147
+ children: React.ReactNode;
7148
+ redirectTo?: string;
7149
+ }
7150
+
7151
+ interface PermissionGuardProps {
7152
+ children: React.ReactNode;
7153
+ permissions: string[];
7154
+ fallback?: React.ReactNode;
7155
+ }
7156
+
7157
+ /**
7158
+ * Protect routes that require authentication
7159
+ */
7160
+ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
7161
+ children,
7162
+ redirectTo = '/login'
7163
+ }) => {
7164
+ const location = useLocation();
7165
+
7166
+ // TODO: Replace with your auth hook
7167
+ const isAuthenticated = true; // useAuth().isAuthenticated;
7168
+
7169
+ if (!isAuthenticated) {
7170
+ return <Navigate to={redirectTo} state={{ from: location }} replace />;
7171
+ }
7172
+
7173
+ return <>{children}</>;
7174
+ };
7175
+
7176
+ /**
7177
+ * Guard routes based on user permissions
7178
+ */
7179
+ export const PermissionGuard: React.FC<PermissionGuardProps> = ({
7180
+ children,
7181
+ permissions,
7182
+ fallback
7183
+ }) => {
7184
+ // TODO: Replace with your auth hook
7185
+ const userPermissions: string[] = []; // useAuth().permissions;
7186
+
7187
+ const hasPermission = permissions.length === 0 ||
7188
+ permissions.some(p => userPermissions.includes(p));
7189
+
7190
+ if (!hasPermission) {
7191
+ if (fallback) return <>{fallback}</>;
7192
+ return (
7193
+ <div className="flex items-center justify-center h-64">
7194
+ <div className="text-center">
7195
+ <h2 className="text-xl font-semibold text-gray-900">Access Denied</h2>
7196
+ <p className="mt-2 text-gray-600">
7197
+ You don't have permission to access this page.
7198
+ </p>
7199
+ <p className="mt-1 text-sm text-gray-500">
7200
+ Required: {permissions.join(', ')}
7201
+ </p>
7202
+ </div>
7203
+ </div>
7204
+ );
7205
+ }
7206
+
7207
+ return <>{children}</>;
7208
+ };
7209
+ `;
7210
+ }
7211
+ function capitalize(str) {
7212
+ return str.charAt(0).toUpperCase() + str.slice(1);
7213
+ }
7214
+ function formatResult5(result, input) {
7215
+ const lines = [];
7216
+ lines.push("# Scaffold Routes");
7217
+ lines.push("");
7218
+ if (input.options?.dryRun) {
7219
+ lines.push("> **DRY RUN** - No files were written");
7220
+ lines.push("");
7221
+ }
7222
+ lines.push("## Configuration");
7223
+ lines.push("");
7224
+ lines.push(`- **Source**: ${input.source}`);
7225
+ lines.push(`- **Scope**: ${input.scope}`);
7226
+ lines.push("");
7227
+ lines.push("## Generated Files");
7228
+ lines.push("");
7229
+ for (const file of result.files) {
7230
+ const relativePath = file.path.replace(/\\/g, "/").split("/src/").pop() || file.path;
7231
+ lines.push(`### ${relativePath}`);
7232
+ lines.push("");
7233
+ lines.push("```tsx");
7234
+ lines.push(file.content.substring(0, 2e3) + (file.content.length > 2e3 ? "\n// ... (truncated)" : ""));
7235
+ lines.push("```");
7236
+ lines.push("");
7237
+ }
7238
+ lines.push("## Instructions");
7239
+ lines.push("");
7240
+ for (const instruction of result.instructions) {
7241
+ lines.push(`- ${instruction}`);
7242
+ }
7243
+ return lines.join("\n");
7244
+ }
7245
+
7246
+ // src/tools/validate-frontend-routes.ts
7247
+ import path16 from "path";
7248
+ import { glob as glob3 } from "glob";
7249
+ var validateFrontendRoutesTool = {
7250
+ name: "validate_frontend_routes",
7251
+ description: `Validate frontend routes against backend NavRoute attributes.
7252
+
7253
+ Checks:
7254
+ - navRoutes.generated.ts exists and is up-to-date
7255
+ - API clients use correct NavRoute paths
7256
+ - React Router configuration matches backend routes
7257
+ - Permission configurations are synchronized
7258
+
7259
+ Example:
7260
+ validate_frontend_routes scope="all"
7261
+
7262
+ Reports issues and provides actionable recommendations for synchronization.`,
7263
+ inputSchema: {
7264
+ type: "object",
7265
+ properties: {
7266
+ scope: {
7267
+ type: "string",
7268
+ enum: ["api-clients", "routes", "registry", "all"],
7269
+ default: "all",
7270
+ description: "Scope of validation"
7271
+ },
7272
+ options: {
7273
+ type: "object",
7274
+ properties: {
7275
+ fix: { type: "boolean", default: false, description: "Auto-fix minor issues" },
7276
+ strict: { type: "boolean", default: false, description: "Fail on warnings" }
7277
+ }
7278
+ }
7279
+ }
7280
+ }
7281
+ };
7282
+ async function handleValidateFrontendRoutes(args, config) {
7283
+ const input = ValidateFrontendRoutesInputSchema.parse(args);
7284
+ logger.info("Validating frontend routes", { scope: input.scope });
7285
+ const result = await validateFrontendRoutes(input, config);
7286
+ return formatResult6(result, input);
7287
+ }
7288
+ async function validateFrontendRoutes(input, config) {
7289
+ const result = {
7290
+ valid: true,
7291
+ registry: {
7292
+ exists: false,
7293
+ routeCount: 0,
7294
+ outdated: []
7295
+ },
7296
+ apiClients: {
7297
+ total: 0,
7298
+ valid: 0,
7299
+ issues: []
7300
+ },
7301
+ routes: {
7302
+ total: 0,
7303
+ orphaned: [],
7304
+ missing: []
7305
+ },
7306
+ recommendations: []
7307
+ };
7308
+ const { scope } = input;
7309
+ const projectRoot = config.smartstack.projectPath;
7310
+ const structure = await findSmartStackStructure(projectRoot);
7311
+ const webPath = structure.web || path16.join(projectRoot, "web", "smartstack-web");
7312
+ const backendRoutes = await discoverBackendNavRoutes(structure);
7313
+ if (scope === "all" || scope === "registry") {
7314
+ await validateRegistry(webPath, backendRoutes, result);
7315
+ }
7316
+ if (scope === "all" || scope === "api-clients") {
7317
+ await validateApiClients(webPath, backendRoutes, result);
7318
+ }
7319
+ if (scope === "all" || scope === "routes") {
7320
+ await validateRoutes(webPath, backendRoutes, result);
7321
+ }
7322
+ generateRecommendations2(result);
7323
+ result.valid = result.apiClients.issues.filter((i) => i.severity === "error").length === 0 && result.routes.missing.length === 0 && result.registry.exists;
7324
+ return result;
7325
+ }
7326
+ async function discoverBackendNavRoutes(structure) {
7327
+ const routes = [];
7328
+ const apiPath = structure.api;
7329
+ if (!apiPath) {
7330
+ return routes;
7331
+ }
7332
+ const controllerFiles = await glob3("**/*Controller.cs", {
7333
+ cwd: apiPath,
7334
+ absolute: true,
7335
+ ignore: ["**/obj/**", "**/bin/**"]
7336
+ });
7337
+ for (const file of controllerFiles) {
7338
+ try {
7339
+ const content = await readText(file);
7340
+ const navRouteMatch = content.match(/\[NavRoute\s*\(\s*"([^"]+)"(?:\s*,\s*Suffix\s*=\s*"([^"]+)")?\s*\)\]/);
7341
+ if (navRouteMatch) {
7342
+ const navRoute = navRouteMatch[1];
7343
+ const suffix = navRouteMatch[2];
7344
+ const fullNavRoute = suffix ? `${navRoute}.${suffix}` : navRoute;
7345
+ const controllerMatch = path16.basename(file).match(/(.+)Controller\.cs$/);
7346
+ const controllerName = controllerMatch ? controllerMatch[1] : "Unknown";
7347
+ const methods = [];
7348
+ if (content.includes("[HttpGet]")) methods.push("GET");
7349
+ if (content.includes("[HttpPost]")) methods.push("POST");
7350
+ if (content.includes("[HttpPut]")) methods.push("PUT");
7351
+ if (content.includes("[HttpDelete]")) methods.push("DELETE");
7352
+ const permissions = [];
7353
+ const authorizeMatches = content.matchAll(/\[Authorize\s*\(\s*[^)]*Policy\s*=\s*"([^"]+)"/g);
7354
+ for (const match of authorizeMatches) {
7355
+ permissions.push(match[1]);
7356
+ }
7357
+ routes.push({
7358
+ navRoute: fullNavRoute,
7359
+ apiPath: `/api/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
7360
+ webPath: `/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
7361
+ permissions,
7362
+ controller: controllerName,
7363
+ methods
7364
+ });
7365
+ }
7366
+ } catch {
7367
+ logger.debug(`Failed to parse controller: ${file}`);
7368
+ }
7369
+ }
7370
+ return routes;
7371
+ }
7372
+ async function validateRegistry(webPath, backendRoutes, result) {
7373
+ const registryPath = path16.join(webPath, "src", "routes", "navRoutes.generated.ts");
7374
+ if (!await fileExists(registryPath)) {
7375
+ result.registry.exists = false;
7376
+ result.recommendations.push("Run `scaffold_routes` to generate navRoutes.generated.ts");
7377
+ return;
7378
+ }
7379
+ result.registry.exists = true;
7380
+ try {
7381
+ const content = await readText(registryPath);
7382
+ const routeMatches = content.matchAll(/'([a-z.]+)':\s*\{/g);
7383
+ const registryRoutes = /* @__PURE__ */ new Set();
7384
+ for (const match of routeMatches) {
7385
+ registryRoutes.add(match[1]);
7386
+ }
7387
+ result.registry.routeCount = registryRoutes.size;
7388
+ for (const backendRoute of backendRoutes) {
7389
+ if (!registryRoutes.has(backendRoute.navRoute)) {
7390
+ result.registry.outdated.push(backendRoute.navRoute);
7391
+ }
7392
+ }
7393
+ for (const registryRoute of registryRoutes) {
7394
+ if (!backendRoutes.find((r) => r.navRoute === registryRoute)) {
7395
+ result.registry.outdated.push(`${registryRoute} (removed from backend)`);
7396
+ }
7397
+ }
7398
+ } catch {
7399
+ result.registry.exists = false;
7400
+ }
7401
+ }
7402
+ async function validateApiClients(webPath, backendRoutes, result) {
7403
+ const servicesPath = path16.join(webPath, "src", "services", "api");
7404
+ const clientFiles = await glob3("**/*.ts", {
7405
+ cwd: servicesPath,
7406
+ absolute: true,
7407
+ ignore: ["**/index.ts"]
7408
+ });
7409
+ result.apiClients.total = clientFiles.length;
7410
+ for (const file of clientFiles) {
7411
+ try {
7412
+ const content = await readText(file);
7413
+ const relativePath = path16.relative(webPath, file);
7414
+ const usesRegistry = content.includes("getRoute('") || content.includes('getRoute("');
7415
+ if (!usesRegistry) {
7416
+ const hardcodedMatch = content.match(/apiClient\.(get|post|put|delete)\s*[<(]\s*['"`]([^'"`]+)['"`]/);
7417
+ if (hardcodedMatch) {
7418
+ result.apiClients.issues.push({
7419
+ type: "invalid-path",
7420
+ severity: "warning",
7421
+ file: relativePath,
7422
+ message: `Hardcoded API path: ${hardcodedMatch[2]}`,
7423
+ suggestion: "Use getRoute() from navRoutes.generated.ts instead"
7424
+ });
7425
+ }
7426
+ } else {
7427
+ const navRouteMatch = content.match(/getRoute\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/);
7428
+ if (navRouteMatch) {
7429
+ const navRoute = navRouteMatch[1];
7430
+ const backendRoute = backendRoutes.find((r) => r.navRoute === navRoute);
7431
+ if (!backendRoute) {
7432
+ result.apiClients.issues.push({
7433
+ type: "missing-route",
7434
+ severity: "error",
7435
+ file: relativePath,
7436
+ navRoute,
7437
+ message: `NavRoute "${navRoute}" not found in backend controllers`,
7438
+ suggestion: "Verify the NavRoute path or update the backend controller"
7439
+ });
7440
+ } else {
7441
+ result.apiClients.valid++;
7442
+ }
7443
+ }
7444
+ }
7445
+ } catch {
7446
+ logger.debug(`Failed to parse API client: ${file}`);
7447
+ }
7448
+ }
7449
+ }
7450
+ async function validateRoutes(webPath, backendRoutes, result) {
7451
+ const routesPath = path16.join(webPath, "src", "routes", "index.tsx");
7452
+ if (!await fileExists(routesPath)) {
7453
+ result.routes.total = 0;
7454
+ result.routes.missing = backendRoutes.map((r) => r.navRoute);
7455
+ return;
7456
+ }
7457
+ try {
7458
+ const content = await readText(routesPath);
7459
+ const pathMatches = content.matchAll(/path:\s*['"`]([^'"`]+)['"`]/g);
7460
+ const frontendPaths = /* @__PURE__ */ new Set();
7461
+ for (const match of pathMatches) {
7462
+ frontendPaths.add(match[1]);
7463
+ }
7464
+ result.routes.total = frontendPaths.size;
7465
+ for (const backendRoute of backendRoutes) {
7466
+ const webPath2 = backendRoute.webPath.replace(/^\//, "");
7467
+ const parts = webPath2.split("/");
7468
+ let found = false;
7469
+ for (const pathPart of parts) {
7470
+ if (frontendPaths.has(pathPart)) {
7471
+ found = true;
7472
+ break;
7473
+ }
7474
+ }
7475
+ if (!found && parts.length > 0) {
7476
+ result.routes.missing.push(backendRoute.navRoute);
7477
+ }
7478
+ }
7479
+ for (const frontendPath of frontendPaths) {
7480
+ if (frontendPath === "*" || frontendPath === "" || frontendPath.startsWith(":")) {
7481
+ continue;
7482
+ }
7483
+ const matchingBackend = backendRoutes.find(
7484
+ (r) => r.webPath.includes(frontendPath) || r.navRoute.includes(frontendPath)
7485
+ );
7486
+ if (!matchingBackend) {
7487
+ result.routes.orphaned.push(frontendPath);
7488
+ }
7489
+ }
7490
+ } catch {
7491
+ result.routes.total = 0;
7492
+ result.routes.missing = backendRoutes.map((r) => r.navRoute);
7493
+ }
7494
+ }
7495
+ function generateRecommendations2(result) {
7496
+ if (!result.registry.exists) {
7497
+ result.recommendations.push('Run `scaffold_routes source="controllers"` to generate route registry');
7498
+ } else if (result.registry.outdated.length > 0) {
7499
+ result.recommendations.push(`Registry is outdated: ${result.registry.outdated.length} routes need sync. Run \`scaffold_routes\``);
7500
+ }
7501
+ if (result.apiClients.issues.length > 0) {
7502
+ const hardcoded = result.apiClients.issues.filter((i) => i.type === "invalid-path").length;
7503
+ const missing = result.apiClients.issues.filter((i) => i.type === "missing-route").length;
7504
+ if (hardcoded > 0) {
7505
+ result.recommendations.push(`${hardcoded} API clients use hardcoded paths. Migrate to getRoute()`);
7506
+ }
7507
+ if (missing > 0) {
7508
+ result.recommendations.push(`${missing} API clients reference non-existent NavRoutes`);
7509
+ }
7510
+ }
7511
+ if (result.routes.missing.length > 0) {
7512
+ result.recommendations.push(`${result.routes.missing.length} backend routes have no frontend counterpart`);
7513
+ }
7514
+ if (result.routes.orphaned.length > 0) {
7515
+ result.recommendations.push(`${result.routes.orphaned.length} frontend routes have no backend NavRoute`);
7516
+ }
7517
+ if (result.valid && result.recommendations.length === 0) {
7518
+ result.recommendations.push("All routes are synchronized between frontend and backend");
7519
+ }
7520
+ }
7521
+ function formatResult6(result, _input) {
7522
+ const lines = [];
7523
+ const statusIcon = result.valid ? "\u2705" : "\u274C";
7524
+ lines.push(`# Frontend Route Validation ${statusIcon}`);
7525
+ lines.push("");
7526
+ lines.push("## Summary");
7527
+ lines.push("");
7528
+ lines.push(`| Metric | Value |`);
7529
+ lines.push(`|--------|-------|`);
7530
+ lines.push(`| Valid | ${result.valid ? "Yes" : "No"} |`);
7531
+ lines.push(`| Registry Exists | ${result.registry.exists ? "Yes" : "No"} |`);
7532
+ lines.push(`| Registry Routes | ${result.registry.routeCount} |`);
7533
+ lines.push(`| API Clients | ${result.apiClients.valid}/${result.apiClients.total} valid |`);
7534
+ lines.push(`| Frontend Routes | ${result.routes.total} |`);
7535
+ lines.push(`| Missing Routes | ${result.routes.missing.length} |`);
7536
+ lines.push(`| Orphaned Routes | ${result.routes.orphaned.length} |`);
7537
+ lines.push("");
7538
+ if (result.registry.outdated.length > 0) {
7539
+ lines.push("## Outdated Registry Entries");
7540
+ lines.push("");
7541
+ for (const route of result.registry.outdated) {
7542
+ lines.push(`- \`${route}\``);
7543
+ }
7544
+ lines.push("");
7545
+ }
7546
+ if (result.apiClients.issues.length > 0) {
7547
+ lines.push("## API Client Issues");
7548
+ lines.push("");
7549
+ for (const issue of result.apiClients.issues) {
7550
+ const icon = issue.severity === "error" ? "\u274C" : "\u26A0\uFE0F";
7551
+ lines.push(`### ${icon} ${issue.file}`);
7552
+ lines.push("");
7553
+ lines.push(`- **Type**: ${issue.type}`);
7554
+ lines.push(`- **Message**: ${issue.message}`);
7555
+ if (issue.navRoute) {
7556
+ lines.push(`- **NavRoute**: \`${issue.navRoute}\``);
7557
+ }
7558
+ lines.push(`- **Suggestion**: ${issue.suggestion}`);
7559
+ lines.push("");
7560
+ }
7561
+ }
7562
+ if (result.routes.missing.length > 0) {
7563
+ lines.push("## Missing Frontend Routes");
7564
+ lines.push("");
7565
+ lines.push("These backend NavRoutes have no corresponding frontend route:");
7566
+ lines.push("");
7567
+ for (const route of result.routes.missing) {
7568
+ lines.push(`- \`${route}\``);
7569
+ }
7570
+ lines.push("");
7571
+ }
7572
+ if (result.routes.orphaned.length > 0) {
7573
+ lines.push("## Orphaned Frontend Routes");
7574
+ lines.push("");
7575
+ lines.push("These frontend routes have no corresponding backend NavRoute:");
7576
+ lines.push("");
7577
+ for (const route of result.routes.orphaned) {
7578
+ lines.push(`- \`${route}\``);
7579
+ }
7580
+ lines.push("");
7581
+ }
7582
+ lines.push("## Recommendations");
7583
+ lines.push("");
7584
+ for (const rec of result.recommendations) {
7585
+ lines.push(`- ${rec}`);
7586
+ }
7587
+ lines.push("");
7588
+ lines.push("## Commands");
7589
+ lines.push("");
7590
+ lines.push("```bash");
7591
+ lines.push("# Regenerate route registry");
7592
+ lines.push('scaffold_routes source="controllers"');
7593
+ lines.push("");
7594
+ lines.push("# Generate API client for a specific NavRoute");
7595
+ lines.push('scaffold_api_client navRoute="platform.administration.users" name="User"');
7596
+ lines.push("```");
7597
+ return lines.join("\n");
7598
+ }
7599
+
6324
7600
  // src/resources/conventions.ts
6325
7601
  var conventionsResourceTemplate = {
6326
7602
  uri: "smartstack://conventions",
@@ -7307,7 +8583,7 @@ Run specific or all checks:
7307
8583
  }
7308
8584
 
7309
8585
  // src/resources/project-info.ts
7310
- import path14 from "path";
8586
+ import path17 from "path";
7311
8587
  var projectInfoResourceTemplate = {
7312
8588
  uri: "smartstack://project",
7313
8589
  name: "SmartStack Project Info",
@@ -7344,16 +8620,16 @@ async function getProjectInfoResource(config) {
7344
8620
  lines.push("```");
7345
8621
  lines.push(`${projectInfo.name}/`);
7346
8622
  if (structure.domain) {
7347
- lines.push(`\u251C\u2500\u2500 ${path14.basename(structure.domain)}/ # Domain layer (entities)`);
8623
+ lines.push(`\u251C\u2500\u2500 ${path17.basename(structure.domain)}/ # Domain layer (entities)`);
7348
8624
  }
7349
8625
  if (structure.application) {
7350
- lines.push(`\u251C\u2500\u2500 ${path14.basename(structure.application)}/ # Application layer (services)`);
8626
+ lines.push(`\u251C\u2500\u2500 ${path17.basename(structure.application)}/ # Application layer (services)`);
7351
8627
  }
7352
8628
  if (structure.infrastructure) {
7353
- lines.push(`\u251C\u2500\u2500 ${path14.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
8629
+ lines.push(`\u251C\u2500\u2500 ${path17.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
7354
8630
  }
7355
8631
  if (structure.api) {
7356
- lines.push(`\u251C\u2500\u2500 ${path14.basename(structure.api)}/ # API layer (controllers)`);
8632
+ lines.push(`\u251C\u2500\u2500 ${path17.basename(structure.api)}/ # API layer (controllers)`);
7357
8633
  }
7358
8634
  if (structure.web) {
7359
8635
  lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
@@ -7366,8 +8642,8 @@ async function getProjectInfoResource(config) {
7366
8642
  lines.push("| Project | Path |");
7367
8643
  lines.push("|---------|------|");
7368
8644
  for (const csproj of projectInfo.csprojFiles) {
7369
- const name = path14.basename(csproj, ".csproj");
7370
- const relativePath = path14.relative(projectPath, csproj);
8645
+ const name = path17.basename(csproj, ".csproj");
8646
+ const relativePath = path17.relative(projectPath, csproj);
7371
8647
  lines.push(`| ${name} | \`${relativePath}\` |`);
7372
8648
  }
7373
8649
  lines.push("");
@@ -7377,10 +8653,10 @@ async function getProjectInfoResource(config) {
7377
8653
  cwd: structure.migrations,
7378
8654
  ignore: ["*.Designer.cs"]
7379
8655
  });
7380
- const migrations = migrationFiles.map((f) => path14.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
8656
+ const migrations = migrationFiles.map((f) => path17.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
7381
8657
  lines.push("## EF Core Migrations");
7382
8658
  lines.push("");
7383
- lines.push(`**Location**: \`${path14.relative(projectPath, structure.migrations)}\``);
8659
+ lines.push(`**Location**: \`${path17.relative(projectPath, structure.migrations)}\``);
7384
8660
  lines.push(`**Total Migrations**: ${migrations.length}`);
7385
8661
  lines.push("");
7386
8662
  if (migrations.length > 0) {
@@ -7415,11 +8691,11 @@ async function getProjectInfoResource(config) {
7415
8691
  lines.push("dotnet build");
7416
8692
  lines.push("");
7417
8693
  lines.push("# Run API");
7418
- lines.push(`cd ${structure.api ? path14.relative(projectPath, structure.api) : "SmartStack.Api"}`);
8694
+ lines.push(`cd ${structure.api ? path17.relative(projectPath, structure.api) : "SmartStack.Api"}`);
7419
8695
  lines.push("dotnet run");
7420
8696
  lines.push("");
7421
8697
  lines.push("# Run frontend");
7422
- lines.push(`cd ${structure.web ? path14.relative(projectPath, structure.web) : "web/smartstack-web"}`);
8698
+ lines.push(`cd ${structure.web ? path17.relative(projectPath, structure.web) : "web/smartstack-web"}`);
7423
8699
  lines.push("npm run dev");
7424
8700
  lines.push("");
7425
8701
  lines.push("# Create migration");
@@ -7442,7 +8718,7 @@ async function getProjectInfoResource(config) {
7442
8718
  }
7443
8719
 
7444
8720
  // src/resources/api-endpoints.ts
7445
- import path15 from "path";
8721
+ import path18 from "path";
7446
8722
  var apiEndpointsResourceTemplate = {
7447
8723
  uri: "smartstack://api/",
7448
8724
  name: "SmartStack API Endpoints",
@@ -7467,7 +8743,7 @@ async function getApiEndpointsResource(config, endpointFilter) {
7467
8743
  }
7468
8744
  async function parseController(filePath, _rootPath) {
7469
8745
  const content = await readText(filePath);
7470
- const fileName = path15.basename(filePath, ".cs");
8746
+ const fileName = path18.basename(filePath, ".cs");
7471
8747
  const controllerName = fileName.replace("Controller", "");
7472
8748
  const endpoints = [];
7473
8749
  const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
@@ -7614,7 +8890,7 @@ function getMethodEmoji(method) {
7614
8890
  }
7615
8891
 
7616
8892
  // src/resources/db-schema.ts
7617
- import path16 from "path";
8893
+ import path19 from "path";
7618
8894
  var dbSchemaResourceTemplate = {
7619
8895
  uri: "smartstack://schema/",
7620
8896
  name: "SmartStack Database Schema",
@@ -7704,7 +8980,7 @@ async function parseEntity(filePath, rootPath, _config) {
7704
8980
  tableName,
7705
8981
  properties,
7706
8982
  relationships,
7707
- file: path16.relative(rootPath, filePath)
8983
+ file: path19.relative(rootPath, filePath)
7708
8984
  };
7709
8985
  }
7710
8986
  async function enrichFromConfigurations(entities, infrastructurePath, _config) {
@@ -7850,7 +9126,7 @@ function formatSchema(entities, filter, _config) {
7850
9126
  }
7851
9127
 
7852
9128
  // src/resources/entities.ts
7853
- import path17 from "path";
9129
+ import path20 from "path";
7854
9130
  var entitiesResourceTemplate = {
7855
9131
  uri: "smartstack://entities/",
7856
9132
  name: "SmartStack Entities",
@@ -7910,7 +9186,7 @@ async function parseEntitySummary(filePath, rootPath, config) {
7910
9186
  hasSoftDelete,
7911
9187
  hasRowVersion,
7912
9188
  file: filePath,
7913
- relativePath: path17.relative(rootPath, filePath)
9189
+ relativePath: path20.relative(rootPath, filePath)
7914
9190
  };
7915
9191
  }
7916
9192
  function inferTableInfo(entityName, config) {
@@ -8102,7 +9378,11 @@ async function createServer() {
8102
9378
  scaffoldTestsTool,
8103
9379
  analyzeTestCoverageTool,
8104
9380
  validateTestConventionsTool,
8105
- suggestTestScenariosTool
9381
+ suggestTestScenariosTool,
9382
+ // Frontend Route Tools
9383
+ scaffoldApiClientTool,
9384
+ scaffoldRoutesTool,
9385
+ validateFrontendRoutesTool
8106
9386
  ]
8107
9387
  };
8108
9388
  });
@@ -8141,6 +9421,16 @@ async function createServer() {
8141
9421
  case "suggest_test_scenarios":
8142
9422
  result = await handleSuggestTestScenarios(args, config);
8143
9423
  break;
9424
+ // Frontend Route Tools
9425
+ case "scaffold_api_client":
9426
+ result = await handleScaffoldApiClient(args ?? {}, config);
9427
+ break;
9428
+ case "scaffold_routes":
9429
+ result = await handleScaffoldRoutes(args ?? {}, config);
9430
+ break;
9431
+ case "validate_frontend_routes":
9432
+ result = await handleValidateFrontendRoutes(args ?? {}, config);
9433
+ break;
8144
9434
  default:
8145
9435
  throw new Error(`Unknown tool: ${name}`);
8146
9436
  }