create-better-fullstack 2.0.3 → 2.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.
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { a as getEffectiveStack, i as writeBtsConfig, l as getGraphSummary, n as readBtsConfig, p as getLatestCLIVersion, t as previewBtsConfigUpdate } from "./bts-config-Bg1Qea9Y.mjs";
2
+ import { f as getEffectiveStack, g as getGraphSummary, n as previewBtsConfigUpdate, o as writeBtsConfig, r as readBtsConfig, s as getLatestCLIVersion } from "./bts-config-InNcw1aJ.mjs";
3
+ import { n as getTemplateDescription, r as generateReproducibleCommand, t as getTemplateConfig } from "./templates-B351F1yX.mjs";
3
4
  import z from "zod";
4
- import { AISchema, APISchema, AddonsSchema, AnalyticsSchema, AnimationSchema, AstroIntegrationSchema, AuthSchema, BackendSchema, CATEGORY_ORDER, CMSSchema, CSSFrameworkSchema, CachingSchema, DatabaseSchema, DatabaseSetupSchema, DotnetApiSchema, DotnetAuthSchema, DotnetCachingSchema, DotnetDeploySchema, DotnetJobQueueSchema, DotnetObservabilitySchema, DotnetOrmSchema, DotnetRealtimeSchema, DotnetTestingSchema, DotnetValidationSchema, DotnetWebFrameworkSchema, EcosystemSchema, EffectSchema, ElixirApiSchema, ElixirAuthSchema, ElixirCachingSchema, ElixirDeploySchema, ElixirEmailSchema, ElixirHttpSchema, ElixirJobsSchema, ElixirJsonSchema, ElixirLibrariesSchema, ElixirObservabilitySchema, ElixirOrmSchema, ElixirQualitySchema, ElixirRealtimeSchema, ElixirTestingSchema, ElixirValidationSchema, ElixirWebFrameworkSchema, EmailSchema, ExamplesSchema, FeatureFlagsSchema, FileStorageSchema, FileUploadSchema, FormsSchema, FrontendSchema, GoApiSchema, GoAuthSchema, GoCachingSchema, GoCliSchema, GoConfigSchema, GoLoggingSchema, GoMessageQueueSchema, GoObservabilitySchema, GoOrmSchema, GoRealtimeSchema, GoTestingSchema, GoWebFrameworkSchema, I18nSchema, JavaApiSchema, JavaAuthSchema, JavaBuildToolSchema, JavaLibrariesSchema, JavaLoggingSchema, JavaOrmSchema, JavaTestingLibrariesSchema, JavaWebFrameworkSchema, JobQueueSchema, LoggingSchema, MobileDeepLinkingSchema, MobileNavigationSchema, MobileOTASchema, MobilePushSchema, MobileStorageSchema, MobileTestingSchema, MobileUISchema, OPTION_CATEGORY_METADATA, ORMSchema, ObservabilitySchema, PackageManagerSchema, PaymentsSchema, PythonAiSchema, PythonApiSchema, PythonAuthSchema, PythonCachingSchema, PythonCliSchema, PythonGraphqlSchema, PythonObservabilitySchema, PythonOrmSchema, PythonQualitySchema, PythonRealtimeSchema, PythonTaskQueueSchema, PythonTestingSchema, PythonValidationSchema, PythonWebFrameworkSchema, RateLimitSchema, RealtimeSchema, RuntimeSchema, RustApiSchema, RustAuthSchema, RustCachingSchema, RustCliSchema, RustErrorHandlingSchema, RustFrontendSchema, RustLibrariesSchema, RustLoggingSchema, RustMessageQueueSchema, RustObservabilitySchema, RustOrmSchema, RustRealtimeSchema, RustTemplatingSchema, RustWebFrameworkSchema, SearchSchema, ServerDeploySchema, StateManagementSchema, TestingSchema, UILibrarySchema, ValidationSchema, WebDeploySchema, analyzeStackCompatibility, formatStackPartSpec, getCategoryOrderForEcosystem, legacyProjectConfigToStackParts } from "@better-fullstack/types";
5
+ import { AISchema, APISchema, AddonsSchema, AiDocsSchema, AnalyticsSchema, AnimationSchema, AstroIntegrationSchema, AuthSchema, BackendSchema, CATEGORY_ORDER, CMSSchema, CSSFrameworkSchema, CachingSchema, DatabaseSchema, DatabaseSetupSchema, DotnetApiSchema, DotnetAuthSchema, DotnetCachingSchema, DotnetDeploySchema, DotnetJobQueueSchema, DotnetObservabilitySchema, DotnetOrmSchema, DotnetRealtimeSchema, DotnetTestingSchema, DotnetValidationSchema, DotnetWebFrameworkSchema, EcosystemSchema, EffectSchema, ElixirApiSchema, ElixirAuthSchema, ElixirCachingSchema, ElixirDeploySchema, ElixirEmailSchema, ElixirHttpSchema, ElixirJobsSchema, ElixirJsonSchema, ElixirLibrariesSchema, ElixirObservabilitySchema, ElixirOrmSchema, ElixirQualitySchema, ElixirRealtimeSchema, ElixirTestingSchema, ElixirValidationSchema, ElixirWebFrameworkSchema, EmailSchema, ExamplesSchema, FeatureFlagsSchema, FileStorageSchema, FileUploadSchema, FormsSchema, FrontendSchema, GoApiSchema, GoAuthSchema, GoCachingSchema, GoCliSchema, GoConfigSchema, GoLoggingSchema, GoMessageQueueSchema, GoObservabilitySchema, GoOrmSchema, GoRealtimeSchema, GoTestingSchema, GoWebFrameworkSchema, I18nSchema, JavaApiSchema, JavaAuthSchema, JavaBuildToolSchema, JavaLibrariesSchema, JavaLoggingSchema, JavaOrmSchema, JavaTestingLibrariesSchema, JavaWebFrameworkSchema, JobQueueSchema, LoggingSchema, MobileDeepLinkingSchema, MobileNavigationSchema, MobileOTASchema, MobilePushSchema, MobileStorageSchema, MobileTestingSchema, MobileUISchema, OPTION_CATEGORY_METADATA, ORMSchema, ObservabilitySchema, PackageManagerSchema, PaymentsSchema, PythonAiSchema, PythonApiSchema, PythonAuthSchema, PythonCachingSchema, PythonCliSchema, PythonGraphqlSchema, PythonObservabilitySchema, PythonOrmSchema, PythonQualitySchema, PythonRealtimeSchema, PythonTaskQueueSchema, PythonTestingSchema, PythonValidationSchema, PythonWebFrameworkSchema, RateLimitSchema, RealtimeSchema, RuntimeSchema, RustApiSchema, RustAuthSchema, RustCachingSchema, RustCliSchema, RustErrorHandlingSchema, RustFrontendSchema, RustLibrariesSchema, RustLoggingSchema, RustMessageQueueSchema, RustObservabilitySchema, RustOrmSchema, RustRealtimeSchema, RustTemplatingSchema, RustWebFrameworkSchema, SearchSchema, ServerDeploySchema, StateManagementSchema, TEMPLATE_VALUES, TestingSchema, UILibrarySchema, ValidationSchema, VectorDbSchema, VersionChannelSchema, WebDeploySchema, analyzeStackCompatibility, evaluateCompatibility, formatStackPartSpec, getCategoryOrderForEcosystem, legacyProjectConfigToStackParts } from "@better-fullstack/types";
5
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
8
 
@@ -56,7 +57,8 @@ function getGuidance() {
56
57
  addons: "ARRAY of strings. Monorepo tools, code quality, desktop (tauri), browser extensions (wxt), etc.",
57
58
  email: "String. TypeScript supports multiple providers; Rust, Python, Go, and Java currently support resend or none.",
58
59
  observability: "String. TypeScript supports multiple providers; Rust, Python, Go, and Java currently support sentry or none.",
59
- search: "String. TypeScript supports multiple providers; Rust, Python, Go, and Java currently support meilisearch or none."
60
+ search: "String. TypeScript supports multiple providers; Rust, Python, Go, and Java currently support meilisearch or none.",
61
+ vectorDb: "String. TypeScript-only vector database for AI embeddings: pgvector, qdrant, chroma, pinecone, or none. Each provider is a standalone service (pgvector connects to a dedicated Postgres+pgvector instance via PGVECTOR_DATABASE_URL). Requires a standalone backend (not convex/none)."
60
62
  },
61
63
  ambiguityRules: [
62
64
  "If the user request leaves major stack choices unspecified, ASK the user before proceeding. Do not guess.",
@@ -245,6 +247,7 @@ const MCP_COMPATIBILITY_DEFAULTS = {
245
247
  shadcnRadius: "default",
246
248
  cms: "none",
247
249
  search: "none",
250
+ vectorDb: "none",
248
251
  fileStorage: "none",
249
252
  mobileUI: "none",
250
253
  mobileStorage: "none",
@@ -255,7 +258,7 @@ const MCP_COMPATIBILITY_DEFAULTS = {
255
258
  versionChannel: "stable",
256
259
  examples: [],
257
260
  aiSdk: "none",
258
- aiDocs: ["claude-md"],
261
+ aiDocs: ["claude-md", "agents-md"],
259
262
  git: "true",
260
263
  install: "false",
261
264
  api: "none",
@@ -376,8 +379,6 @@ function buildProjectConfig(input, overrides) {
376
379
  cssFramework: input.cssFramework ?? (ecosystem === "react-native" ? "none" : defaults.cssFramework),
377
380
  mobileNavigation: input.mobileNavigation ?? (hasMobileProject ? "expo-router" : "none"),
378
381
  mobileDeepLinking: input.mobileDeepLinking ?? (hasMobileProject ? "expo-linking" : "none"),
379
- astroIntegration: "none",
380
- versionChannel: "stable",
381
382
  shadcnBase: "radix",
382
383
  shadcnStyle: "nova",
383
384
  shadcnIconLibrary: "lucide",
@@ -385,7 +386,7 @@ function buildProjectConfig(input, overrides) {
385
386
  shadcnBaseColor: "neutral",
386
387
  shadcnFont: "inter",
387
388
  shadcnRadius: "default",
388
- aiDocs: ["claude-md"],
389
+ aiDocs: input.aiDocs ?? ["claude-md", "agents-md"],
389
390
  git: !!overrides,
390
391
  install: false
391
392
  };
@@ -565,6 +566,279 @@ const GETTING_STARTED_MD = `# Getting Started with Better-Fullstack MCP
565
566
  2. Provide addons array with features to add (e.g., ["biome", "turborepo"]).
566
567
  3. Service categories such as email and observability are scaffold-time options. To add those to an existing app, inspect the generated templates from bfs_plan_project and apply the equivalent dependency, env var, and initialization changes manually.
567
568
  `;
569
+ const guidanceOutputSchema = {
570
+ workflow: z.array(z.string()),
571
+ ecosystems: z.record(z.string(), z.string()),
572
+ fieldRules: z.record(z.string(), z.string()),
573
+ ambiguityRules: z.array(z.string()),
574
+ criticalConstraints: z.array(z.string())
575
+ };
576
+ const schemaOutputSchema = {
577
+ category: z.string().optional(),
578
+ options: z.array(z.string()).optional(),
579
+ categories: z.record(z.string(), z.array(z.string())).optional(),
580
+ error: z.string().optional()
581
+ };
582
+ const compatibilityIssueOutputSchema = z.object({
583
+ code: z.string(),
584
+ message: z.string(),
585
+ category: z.string().optional(),
586
+ optionId: z.string().optional(),
587
+ provided: z.record(z.string(), z.union([z.string(), z.array(z.string())])).optional(),
588
+ suggestions: z.array(z.string()).optional()
589
+ });
590
+ const compatibilityOutputSchema = {
591
+ adjustedStack: z.record(z.string(), z.unknown()).nullable(),
592
+ changes: z.array(z.object({
593
+ category: z.string(),
594
+ message: z.string()
595
+ })),
596
+ issues: z.array(compatibilityIssueOutputSchema),
597
+ hasIssues: z.boolean()
598
+ };
599
+ const graphPreviewOutputShape = {
600
+ graphSummary: z.string().optional(),
601
+ effectiveStack: z.record(z.string(), z.string()).optional(),
602
+ stackPartSpecs: z.array(z.string()).optional()
603
+ };
604
+ const planProjectOutputSchema = {
605
+ success: z.boolean(),
606
+ fileCount: z.number().optional(),
607
+ directoryCount: z.number().optional(),
608
+ files: z.array(z.string()).optional(),
609
+ ...graphPreviewOutputShape
610
+ };
611
+ const createProjectOutputSchema = {
612
+ success: z.boolean(),
613
+ projectDirectory: z.string().optional(),
614
+ fileCount: z.number().optional(),
615
+ addonWarnings: z.array(z.string()).optional(),
616
+ message: z.string().optional(),
617
+ ...graphPreviewOutputShape
618
+ };
619
+ const planAdditionOutputSchema = {
620
+ success: z.boolean(),
621
+ existingConfig: z.object({
622
+ ecosystem: z.string(),
623
+ frontend: z.array(z.string()).optional(),
624
+ backend: z.string().optional(),
625
+ addons: z.array(z.string()).optional(),
626
+ graphSummary: z.string().optional(),
627
+ effectiveStack: z.record(z.string(), z.string()).optional(),
628
+ stackPartSpecs: z.array(z.string())
629
+ }).optional(),
630
+ proposedAdditions: z.object({
631
+ newAddons: z.array(z.string()),
632
+ webDeploy: z.string().nullable(),
633
+ serverDeploy: z.string().nullable(),
634
+ graphSummary: z.string().optional(),
635
+ effectiveStack: z.record(z.string(), z.string()).optional(),
636
+ stackPartSpecs: z.array(z.string())
637
+ }).optional(),
638
+ alreadyPresent: z.array(z.string()).optional(),
639
+ compatibilityWarnings: z.array(z.string()).optional()
640
+ };
641
+ const addFeatureOutputSchema = {
642
+ success: z.boolean(),
643
+ addedAddons: z.array(z.string()).optional(),
644
+ projectDir: z.string().optional(),
645
+ message: z.string().optional(),
646
+ ...graphPreviewOutputShape
647
+ };
648
+ function buildPresetStackSummary(config) {
649
+ const parts = [];
650
+ const frontend = (config.frontend ?? []).filter((item) => item !== "none");
651
+ if (frontend.length > 0) parts.push(`frontend: ${frontend.join("+")}`);
652
+ if (config.backend && config.backend !== "none") parts.push(`backend: ${config.backend}`);
653
+ if (config.runtime && config.runtime !== "none") parts.push(`runtime: ${config.runtime}`);
654
+ if (config.database && config.database !== "none") parts.push(`database: ${config.database}`);
655
+ if (config.orm && config.orm !== "none") parts.push(`orm: ${config.orm}`);
656
+ if (config.api && config.api !== "none") parts.push(`api: ${config.api}`);
657
+ if (config.auth && config.auth !== "none") parts.push(`auth: ${config.auth}`);
658
+ if (config.payments && config.payments !== "none") parts.push(`payments: ${config.payments}`);
659
+ const addons = (config.addons ?? []).filter((item) => item !== "none");
660
+ if (addons.length > 0) parts.push(`addons: ${addons.join("+")}`);
661
+ return parts.join(", ");
662
+ }
663
+ function listMcpPresets() {
664
+ const presets = [];
665
+ for (const id of TEMPLATE_VALUES) {
666
+ if (id === "none") continue;
667
+ const config = getTemplateConfig(id);
668
+ if (!config) continue;
669
+ const ecosystem = (config.frontend ?? []).some((item) => item.startsWith("native-")) ? "react-native" : "typescript";
670
+ presets.push({
671
+ id,
672
+ name: id.toUpperCase(),
673
+ description: getTemplateDescription(id),
674
+ ecosystem,
675
+ stackSummary: buildPresetStackSummary(config),
676
+ stack: config
677
+ });
678
+ }
679
+ return presets;
680
+ }
681
+ function briefMatches(text, keywords) {
682
+ const tokens = new Set(text.split(/[^a-z0-9]+/i).filter(Boolean));
683
+ return keywords.some((keyword) => keyword.includes(" ") ? text.includes(keyword) : tokens.has(keyword));
684
+ }
685
+ function matchNearestPreset(input) {
686
+ const signatureKeys = [
687
+ "database",
688
+ "backend",
689
+ "api",
690
+ "auth"
691
+ ];
692
+ const inputFrontend = input.frontend?.[0];
693
+ let best = null;
694
+ for (const id of TEMPLATE_VALUES) {
695
+ if (id === "none") continue;
696
+ const config = getTemplateConfig(id);
697
+ if (!config) continue;
698
+ let score = 0;
699
+ for (const key of signatureKeys) {
700
+ const value = config[key];
701
+ if (value !== void 0 && value === input[key]) score += 1;
702
+ }
703
+ const presetFrontend = (config.frontend ?? [])[0];
704
+ if (presetFrontend && presetFrontend === inputFrontend) score += 1;
705
+ if (!best || score > best.score) best = {
706
+ id,
707
+ score
708
+ };
709
+ }
710
+ return best && best.score >= 3 ? best.id : null;
711
+ }
712
+ function recommendStackFromBrief(brief, ecosystemHint) {
713
+ const text = brief.toLowerCase();
714
+ const has = (...keywords) => briefMatches(text, keywords);
715
+ const rationale = [];
716
+ const input = {};
717
+ if (ecosystemHint && ecosystemHint !== "typescript") {
718
+ input.ecosystem = ecosystemHint;
719
+ rationale.push(`Ecosystem forced to ${ecosystemHint} from the provided hint; using ${ecosystemHint} defaults.`);
720
+ rationale.push(`Brief keyword analysis (database/auth/payments/AI feature detection) currently applies to the TypeScript ecosystem only — configure those features explicitly for ${ecosystemHint} via bfs_check_compatibility.`);
721
+ return {
722
+ input,
723
+ rationale,
724
+ matchedPreset: null
725
+ };
726
+ }
727
+ if (has("mobile", "ios", "android", "expo") || text.includes("react native") || text.includes("react-native") || text.includes("app store") || text.includes("play store")) {
728
+ input.ecosystem = "react-native";
729
+ input.frontend = ["native-uniwind"];
730
+ input.backend = "none";
731
+ input.runtime = "none";
732
+ input.api = "none";
733
+ input.database = "none";
734
+ input.orm = "none";
735
+ rationale.push("Mobile app detected: React Native (Expo) with the native-uniwind styling preset and no bundled backend.");
736
+ return {
737
+ input,
738
+ rationale,
739
+ matchedPreset: "uniwind"
740
+ };
741
+ }
742
+ input.ecosystem = "typescript";
743
+ input.frontend = ["tanstack-router"];
744
+ input.backend = "hono";
745
+ input.runtime = "bun";
746
+ input.database = "sqlite";
747
+ input.orm = "drizzle";
748
+ input.api = "trpc";
749
+ rationale.push("Default TypeScript fullstack baseline: TanStack Router + Hono + tRPC on SQLite/Drizzle (Bun).");
750
+ if (has("postgres", "postgresql", "supabase", "neon")) {
751
+ input.database = "postgres";
752
+ input.orm = "drizzle";
753
+ rationale.push("Postgres requested: database=postgres, orm=drizzle.");
754
+ } else if (has("mysql", "planetscale")) {
755
+ input.database = "mysql";
756
+ input.orm = "drizzle";
757
+ rationale.push("MySQL requested: database=mysql, orm=drizzle.");
758
+ } else if (has("mongo", "mongodb")) {
759
+ input.database = "mongodb";
760
+ input.orm = "mongoose";
761
+ rationale.push("MongoDB requested: database=mongodb, orm=mongoose.");
762
+ }
763
+ if (has("saas", "payment", "payments", "billing", "subscription", "subscriptions", "checkout", "stripe", "ecommerce")) {
764
+ input.payments = "stripe";
765
+ input.auth = "better-auth";
766
+ if (input.database === "sqlite") {
767
+ input.database = "postgres";
768
+ input.orm = "drizzle";
769
+ }
770
+ rationale.push("SaaS/payments detected: Stripe + better-auth, upgraded to Postgres/Drizzle for production data.");
771
+ } else if (has("auth", "login", "signin", "signup", "account", "accounts", "user", "users", "authentication", "admin", "dashboard", "portal", "members", "rbac", "permissions", "roles")) {
772
+ input.auth = "better-auth";
773
+ rationale.push("Authentication requested: auth=better-auth.");
774
+ }
775
+ if (has("ai", "chatbot", "llm", "gpt", "rag", "agent", "agents", "openai", "assistant", "copilot")) {
776
+ input.ai = "vercel-ai";
777
+ input.examples = ["ai"];
778
+ rationale.push("AI/chatbot detected: Vercel AI SDK with the bundled AI example.");
779
+ }
780
+ if (has("blog", "content", "cms", "marketing", "landing", "publishing")) {
781
+ input.cms = "sanity";
782
+ rationale.push("Content/marketing site detected: Sanity CMS.");
783
+ }
784
+ if (has("realtime", "collaborative", "collaboration", "multiplayer", "presence") || text.includes("real-time") || text.includes("real time")) {
785
+ input.realtime = "socket-io";
786
+ rationale.push("Realtime/collaboration detected: Socket.IO.");
787
+ }
788
+ let matchedPreset = null;
789
+ if (has("t3")) matchedPreset = "t3";
790
+ else if (has("mern")) matchedPreset = "mern";
791
+ else if (has("pern")) matchedPreset = "pern";
792
+ else matchedPreset = matchNearestPreset(input);
793
+ if (matchedPreset) rationale.push(`Closest ready-made preset: ${matchedPreset}.`);
794
+ return {
795
+ input,
796
+ rationale,
797
+ matchedPreset
798
+ };
799
+ }
800
+ function normalizeAdjustedToInput(adjusted, base) {
801
+ const webFrontend = adjusted.webFrontend ?? [];
802
+ const nativeFrontend = adjusted.nativeFrontend ?? [];
803
+ const frontend = [...webFrontend, ...nativeFrontend];
804
+ const codeQuality = adjusted.codeQuality ?? [];
805
+ const documentation = adjusted.documentation ?? [];
806
+ const appPlatforms = adjusted.appPlatforms ?? [];
807
+ return {
808
+ ...adjusted,
809
+ projectName: base.projectName,
810
+ ecosystem: adjusted.ecosystem ?? base.ecosystem,
811
+ frontend: frontend.length > 0 ? frontend : base.frontend,
812
+ addons: [
813
+ ...codeQuality,
814
+ ...documentation,
815
+ ...appPlatforms
816
+ ],
817
+ ai: adjusted.aiSdk ?? base.ai
818
+ };
819
+ }
820
+ function summarizeRecommendedConfig(config) {
821
+ const isTsWeb = config.ecosystem === "typescript" || config.ecosystem === "react-native";
822
+ return {
823
+ projectName: config.projectName,
824
+ ecosystem: config.ecosystem,
825
+ ...isTsWeb ? {
826
+ frontend: config.frontend,
827
+ backend: config.backend,
828
+ runtime: config.runtime,
829
+ api: config.api
830
+ } : {},
831
+ database: config.database,
832
+ orm: config.orm,
833
+ auth: config.auth,
834
+ payments: config.payments,
835
+ ai: config.ai,
836
+ cms: config.cms,
837
+ realtime: config.realtime,
838
+ examples: config.examples,
839
+ addons: config.addons
840
+ };
841
+ }
568
842
  async function startMcpServer() {
569
843
  const server = new McpServer({
570
844
  name: "better-fullstack",
@@ -573,24 +847,120 @@ async function startMcpServer() {
573
847
  instructions: INSTRUCTIONS,
574
848
  capabilities: { logging: {} }
575
849
  });
576
- const registerTool = server.tool.bind(server);
577
- registerTool("bfs_get_guidance", "Returns workflow rules, field semantics, ambiguity rules, and critical constraints. Call this FIRST before using other tools.", mcpInputSchema({}), async () => {
850
+ const registerTool = (name, config, cb) => {
851
+ server.registerTool(name, config, cb);
852
+ };
853
+ registerTool("bfs_get_guidance", {
854
+ description: "Returns workflow rules, field semantics, ambiguity rules, and critical constraints. Call this FIRST before using other tools.",
855
+ inputSchema: mcpInputSchema({}),
856
+ outputSchema: guidanceOutputSchema,
857
+ annotations: {
858
+ title: "Get guidance",
859
+ readOnlyHint: true,
860
+ idempotentHint: true,
861
+ openWorldHint: false
862
+ }
863
+ }, async () => {
578
864
  const guidance = getGuidance();
579
- return { content: [{
580
- type: "text",
581
- text: JSON.stringify(guidance, null, 2)
582
- }] };
865
+ return {
866
+ content: [{
867
+ type: "text",
868
+ text: JSON.stringify(guidance, null, 2)
869
+ }],
870
+ structuredContent: guidance
871
+ };
583
872
  });
584
- registerTool("bfs_get_schema", "Returns valid options for a specific category (e.g., 'database', 'frontend', 'backend') or ALL categories. Use ecosystem to filter to relevant categories only.", mcpInputSchema({
585
- category: z.string().optional().describe("Category name (e.g., 'database', 'orm', 'frontend'). Omit for all categories."),
586
- ecosystem: EcosystemSchema.optional().describe("Filter categories to this ecosystem (e.g., 'rust' returns only Rust + shared categories).")
587
- }), async ({ category, ecosystem }) => {
873
+ registerTool("bfs_get_schema", {
874
+ description: "Returns valid options for a specific category (e.g., 'database', 'frontend', 'backend') or ALL categories. Use ecosystem to filter to relevant categories only.",
875
+ inputSchema: mcpInputSchema({
876
+ category: z.string().optional().describe("Category name (e.g., 'database', 'orm', 'frontend'). Omit for all categories."),
877
+ ecosystem: EcosystemSchema.optional().describe("Filter categories to this ecosystem (e.g., 'rust' returns only Rust + shared categories).")
878
+ }),
879
+ outputSchema: schemaOutputSchema,
880
+ annotations: {
881
+ title: "Get schema options",
882
+ readOnlyHint: true,
883
+ idempotentHint: true,
884
+ openWorldHint: false
885
+ }
886
+ }, async ({ category, ecosystem }) => {
588
887
  const result = getSchemaOptions(category, ecosystem);
888
+ const structuredContent = "error" in result ? { error: result.error } : "category" in result ? {
889
+ category: result.category,
890
+ options: result.options
891
+ } : { categories: result };
892
+ return {
893
+ content: [{
894
+ type: "text",
895
+ text: JSON.stringify(result, null, 2)
896
+ }],
897
+ structuredContent
898
+ };
899
+ });
900
+ registerTool("bfs_list_presets", {
901
+ description: "Lists the ready-made stack presets available to the CLI (mern, pern, t3, uniwind) with id, name, description, ecosystem, and a stack summary. Use to discover a starting point before bfs_recommend_stack, bfs_plan_project, or bfs_create_project.",
902
+ inputSchema: mcpInputSchema({}),
903
+ annotations: {
904
+ title: "List presets",
905
+ readOnlyHint: true,
906
+ idempotentHint: true,
907
+ openWorldHint: false
908
+ }
909
+ }, async () => {
910
+ const presets = listMcpPresets();
589
911
  return { content: [{
590
912
  type: "text",
591
- text: JSON.stringify(result, null, 2)
913
+ text: JSON.stringify({ presets }, null, 2)
592
914
  }] };
593
915
  });
916
+ registerTool("bfs_recommend_stack", {
917
+ description: "Recommends a compatibility-validated stack from a natural-language brief using deterministic keyword rules (no LLM). Returns the config, rationale, any auto-applied compatibility adjustments, the nearest matching preset, and a reproducible CLI command.",
918
+ inputSchema: mcpInputSchema({
919
+ brief: z.string().describe("Natural-language description of the app to build (e.g., 'a SaaS with payments and auth')."),
920
+ ecosystem: EcosystemSchema.optional().describe("Force a language ecosystem. Omit to let the brief decide (defaults to TypeScript)."),
921
+ projectName: z.string().optional().describe("Project name (kebab-case). Default: 'my-app'.")
922
+ }),
923
+ annotations: {
924
+ title: "Recommend stack",
925
+ readOnlyHint: true,
926
+ idempotentHint: false,
927
+ openWorldHint: false
928
+ }
929
+ }, async ({ brief, ecosystem, projectName }) => {
930
+ try {
931
+ const { input: recommended, rationale, matchedPreset } = recommendStackFromBrief(brief, ecosystem);
932
+ const baseInput = {
933
+ projectName: projectName ?? "my-app",
934
+ ...recommended
935
+ };
936
+ const compatResult = analyzeStackCompatibility(buildCompatibilityInput(baseInput));
937
+ const finalConfig = buildProjectConfig(compatResult.adjustedStack ? normalizeAdjustedToInput(compatResult.adjustedStack, baseInput) : baseInput, { projectDir: `/${baseInput.projectName}` });
938
+ const adjustments = compatResult.changes.map((change) => `${change.category}: ${change.message}`);
939
+ const graphPreview = getMcpGraphPreview(finalConfig);
940
+ const reproducibleCommand = generateReproducibleCommand(finalConfig);
941
+ return { content: [{
942
+ type: "text",
943
+ text: JSON.stringify({
944
+ brief,
945
+ config: summarizeRecommendedConfig(finalConfig),
946
+ rationale,
947
+ adjustments,
948
+ matchedPreset,
949
+ reproducibleCommand,
950
+ ...graphPreview,
951
+ nextSteps: "Call bfs_plan_project with this config to preview the files, then bfs_create_project to scaffold it."
952
+ }, null, 2)
953
+ }] };
954
+ } catch (error) {
955
+ return {
956
+ content: [{
957
+ type: "text",
958
+ text: `Recommend stack failed: ${error instanceof Error ? error.message : String(error)}`
959
+ }],
960
+ isError: true
961
+ };
962
+ }
963
+ });
594
964
  const mobileInputSchema = {
595
965
  mobileNavigation: MobileNavigationSchema.optional().describe("Mobile navigation"),
596
966
  mobileUI: MobileUISchema.optional().describe("Mobile UI"),
@@ -682,52 +1052,80 @@ async function startMcpServer() {
682
1052
  elixirDeploy: ElixirDeploySchema.optional().describe("Elixir deployment target"),
683
1053
  elixirLibraries: z.array(ElixirLibrariesSchema).optional().describe("Elixir libraries")
684
1054
  };
685
- registerTool("bfs_check_compatibility", "Validates a stack combination and returns auto-adjusted selections with warnings. Call BEFORE creating a project to avoid invalid combinations.", mcpInputSchema({
686
- ecosystem: EcosystemSchema.describe("Language ecosystem"),
687
- frontend: z.array(z.string()).optional().describe("Web frontend frameworks (TypeScript only)"),
688
- backend: z.string().optional().describe("Backend framework"),
689
- runtime: z.string().optional().describe("JavaScript runtime"),
690
- database: z.string().optional().describe("Database type"),
691
- orm: z.string().optional().describe("ORM"),
692
- api: z.string().optional().describe("API layer"),
693
- auth: z.string().optional().describe("Auth provider"),
694
- payments: z.string().optional().describe("Payments provider"),
695
- email: EmailSchema.optional().describe("Email provider"),
696
- fileUpload: FileUploadSchema.optional().describe("File upload provider"),
697
- ai: AISchema.optional().describe("AI SDK"),
698
- stateManagement: StateManagementSchema.optional().describe("State management"),
699
- forms: FormsSchema.optional().describe("Forms library"),
700
- validation: ValidationSchema.optional().describe("Validation library"),
701
- testing: TestingSchema.optional().describe("Testing framework"),
702
- realtime: RealtimeSchema.optional().describe("Realtime library"),
703
- jobQueue: JobQueueSchema.optional().describe("Job queue"),
704
- animation: AnimationSchema.optional().describe("Animation library"),
705
- logging: LoggingSchema.optional().describe("Logging library"),
706
- observability: ObservabilitySchema.optional().describe("Observability provider"),
707
- featureFlags: FeatureFlagsSchema.optional().describe("Feature flags provider"),
708
- analytics: AnalyticsSchema.optional().describe("Analytics provider"),
709
- cms: CMSSchema.optional().describe("CMS"),
710
- caching: CachingSchema.optional().describe("Caching solution"),
711
- rateLimit: RateLimitSchema.optional().describe("Rate limiting solution"),
712
- i18n: I18nSchema.optional().describe("Internationalization library"),
713
- search: SearchSchema.optional().describe("Search engine"),
714
- fileStorage: FileStorageSchema.optional().describe("File storage"),
715
- ...mobileInputSchema,
716
- ...deploymentInputSchema,
717
- astroIntegration: AstroIntegrationSchema.optional().describe("Astro UI framework integration"),
718
- uiLibrary: z.string().optional().describe("UI component library"),
719
- cssFramework: z.string().optional().describe("CSS framework"),
720
- addons: z.array(AddonsSchema).optional().describe("Addon list"),
721
- examples: z.array(ExamplesSchema).optional().describe("Example templates"),
722
- packageManager: PackageManagerSchema.optional().describe("Package manager"),
723
- ...crossEcosystemInputSchema
724
- }), async (input) => {
1055
+ registerTool("bfs_check_compatibility", {
1056
+ description: "Validates a stack combination and returns auto-adjusted selections with warnings. Call BEFORE creating a project to avoid invalid combinations.",
1057
+ outputSchema: compatibilityOutputSchema,
1058
+ annotations: {
1059
+ title: "Check stack compatibility",
1060
+ readOnlyHint: true,
1061
+ idempotentHint: true,
1062
+ openWorldHint: false
1063
+ },
1064
+ inputSchema: mcpInputSchema({
1065
+ ecosystem: EcosystemSchema.describe("Language ecosystem"),
1066
+ frontend: z.array(z.string()).optional().describe("Web frontend frameworks (TypeScript only)"),
1067
+ backend: z.string().optional().describe("Backend framework"),
1068
+ runtime: z.string().optional().describe("JavaScript runtime"),
1069
+ database: z.string().optional().describe("Database type"),
1070
+ orm: z.string().optional().describe("ORM"),
1071
+ api: z.string().optional().describe("API layer"),
1072
+ auth: z.string().optional().describe("Auth provider"),
1073
+ payments: z.string().optional().describe("Payments provider"),
1074
+ email: EmailSchema.optional().describe("Email provider"),
1075
+ fileUpload: FileUploadSchema.optional().describe("File upload provider"),
1076
+ ai: AISchema.optional().describe("AI SDK"),
1077
+ stateManagement: StateManagementSchema.optional().describe("State management"),
1078
+ forms: FormsSchema.optional().describe("Forms library"),
1079
+ validation: ValidationSchema.optional().describe("Validation library"),
1080
+ testing: TestingSchema.optional().describe("Testing framework"),
1081
+ realtime: RealtimeSchema.optional().describe("Realtime library"),
1082
+ jobQueue: JobQueueSchema.optional().describe("Job queue"),
1083
+ animation: AnimationSchema.optional().describe("Animation library"),
1084
+ logging: LoggingSchema.optional().describe("Logging library"),
1085
+ observability: ObservabilitySchema.optional().describe("Observability provider"),
1086
+ featureFlags: FeatureFlagsSchema.optional().describe("Feature flags provider"),
1087
+ analytics: AnalyticsSchema.optional().describe("Analytics provider"),
1088
+ cms: CMSSchema.optional().describe("CMS"),
1089
+ caching: CachingSchema.optional().describe("Caching solution"),
1090
+ rateLimit: RateLimitSchema.optional().describe("Rate limiting solution"),
1091
+ i18n: I18nSchema.optional().describe("Internationalization library"),
1092
+ search: SearchSchema.optional().describe("Search engine"),
1093
+ vectorDb: VectorDbSchema.optional().describe("Vector database (TypeScript only)"),
1094
+ fileStorage: FileStorageSchema.optional().describe("File storage"),
1095
+ ...mobileInputSchema,
1096
+ ...deploymentInputSchema,
1097
+ astroIntegration: AstroIntegrationSchema.optional().describe("Astro UI framework integration"),
1098
+ uiLibrary: z.string().optional().describe("UI component library"),
1099
+ cssFramework: z.string().optional().describe("CSS framework"),
1100
+ addons: z.array(AddonsSchema).optional().describe("Addon list"),
1101
+ examples: z.array(ExamplesSchema).optional().describe("Example templates"),
1102
+ packageManager: PackageManagerSchema.optional().describe("Package manager"),
1103
+ ...crossEcosystemInputSchema
1104
+ })
1105
+ }, async (input) => {
725
1106
  try {
726
- const filtered = filterCompatibilityResult(analyzeStackCompatibility(buildCompatibilityInput(input)), input.ecosystem);
727
- return { content: [{
728
- type: "text",
729
- text: JSON.stringify(filtered, null, 2)
730
- }] };
1107
+ const compatInput = buildCompatibilityInput(input);
1108
+ const filtered = filterCompatibilityResult(analyzeStackCompatibility(compatInput), input.ecosystem);
1109
+ const evaluation = evaluateCompatibility(compatInput);
1110
+ const relevantEcosystem = isMcpEcosystem(input.ecosystem) ? input.ecosystem : "typescript";
1111
+ const relevantIssueKeys = new Set([...getMcpCategoryKeysForEcosystem(relevantEcosystem), ...MCP_SHARED_COMPATIBILITY_KEYS]);
1112
+ const issues = evaluation.issues.filter((issue) => !issue.category || relevantIssueKeys.has(issue.category));
1113
+ const structuredContent = {
1114
+ adjustedStack: filtered.adjustedStack,
1115
+ changes: filtered.changes,
1116
+ issues,
1117
+ hasIssues: issues.length > 0
1118
+ };
1119
+ return {
1120
+ content: [{
1121
+ type: "text",
1122
+ text: JSON.stringify({
1123
+ ...filtered,
1124
+ issues
1125
+ }, null, 2)
1126
+ }],
1127
+ structuredContent
1128
+ };
731
1129
  } catch (error) {
732
1130
  return {
733
1131
  content: [{
@@ -767,6 +1165,7 @@ async function startMcpServer() {
767
1165
  observability: ObservabilitySchema.optional().describe("Observability"),
768
1166
  featureFlags: FeatureFlagsSchema.optional().describe("Feature flag provider"),
769
1167
  search: SearchSchema.optional().describe("Search engine"),
1168
+ vectorDb: VectorDbSchema.optional().describe("Vector database (TypeScript only)"),
770
1169
  caching: CachingSchema.optional().describe("Caching solution"),
771
1170
  rateLimit: RateLimitSchema.optional().describe("Rate limiting solution"),
772
1171
  i18n: I18nSchema.optional().describe("Internationalization (i18n) library"),
@@ -775,9 +1174,24 @@ async function startMcpServer() {
775
1174
  ...mobileInputSchema,
776
1175
  fileUpload: FileUploadSchema.optional().describe("File upload"),
777
1176
  ...deploymentInputSchema,
1177
+ effect: EffectSchema.optional().describe("Effect ecosystem (effect, effect-full)"),
1178
+ analytics: AnalyticsSchema.optional().describe("Privacy-focused analytics provider"),
1179
+ astroIntegration: AstroIntegrationSchema.optional().describe("Astro UI framework integration (react, vue, svelte, solid)"),
1180
+ aiDocs: z.array(AiDocsSchema).optional().describe("AI documentation files (claude-md, agents-md, cursorrules)"),
1181
+ versionChannel: VersionChannelSchema.optional().describe("Dependency version channel (stable, latest, beta)"),
778
1182
  ...crossEcosystemInputSchema
779
1183
  };
780
- registerTool("bfs_plan_project", "Dry-run: generates a project in-memory and returns the file tree WITHOUT writing to disk. Use this to preview what would be created.", mcpInputSchema(planCreateSchema), async (input) => {
1184
+ registerTool("bfs_plan_project", {
1185
+ description: "Dry-run: generates a project in-memory and returns the file tree WITHOUT writing to disk. Use this to preview what would be created.",
1186
+ inputSchema: mcpInputSchema(planCreateSchema),
1187
+ outputSchema: planProjectOutputSchema,
1188
+ annotations: {
1189
+ title: "Plan project (dry run)",
1190
+ readOnlyHint: true,
1191
+ idempotentHint: true,
1192
+ openWorldHint: false
1193
+ }
1194
+ }, async (input) => {
781
1195
  try {
782
1196
  const { generateVirtualProject, EMBEDDED_TEMPLATES } = await import("@better-fullstack/template-generator");
783
1197
  const config = buildProjectConfig(input);
@@ -788,14 +1202,18 @@ async function startMcpServer() {
788
1202
  if (result.success && result.tree) {
789
1203
  const summary = summarizeTree(result.tree);
790
1204
  const graphPreview = getMcpGraphPreview(config);
791
- return { content: [{
792
- type: "text",
793
- text: JSON.stringify({
794
- success: true,
795
- ...summary,
796
- ...graphPreview
797
- }, null, 2)
798
- }] };
1205
+ const payload = {
1206
+ success: true,
1207
+ ...summary,
1208
+ ...graphPreview
1209
+ };
1210
+ return {
1211
+ content: [{
1212
+ type: "text",
1213
+ text: JSON.stringify(payload, null, 2)
1214
+ }],
1215
+ structuredContent: payload
1216
+ };
799
1217
  }
800
1218
  return {
801
1219
  content: [{
@@ -817,16 +1235,29 @@ async function startMcpServer() {
817
1235
  };
818
1236
  }
819
1237
  });
820
- registerTool("bfs_create_project", "Creates a new fullstack project on disk. Dependencies are NOT installed (agent must tell user to install manually). Call bfs_plan_project first to preview.", mcpInputSchema({
821
- ...planCreateSchema,
822
- projectName: z.string().describe("Project name (kebab-case). Will be the directory name.")
823
- }), async (input) => {
1238
+ registerTool("bfs_create_project", {
1239
+ description: "Creates a new fullstack project on disk. Dependencies are NOT installed (agent must tell user to install manually). Call bfs_plan_project first to preview.",
1240
+ inputSchema: mcpInputSchema({
1241
+ ...planCreateSchema,
1242
+ projectName: z.string().describe("Project name (kebab-case). Will be the directory name."),
1243
+ targetDir: z.string().optional().describe("Absolute path to the parent directory in which to create the project folder (default: current working directory).")
1244
+ }),
1245
+ outputSchema: createProjectOutputSchema,
1246
+ annotations: {
1247
+ title: "Create project",
1248
+ readOnlyHint: false,
1249
+ destructiveHint: false,
1250
+ idempotentHint: false,
1251
+ openWorldHint: false
1252
+ }
1253
+ }, async (input) => {
824
1254
  try {
825
1255
  const { generateVirtualProject, EMBEDDED_TEMPLATES } = await import("@better-fullstack/template-generator");
826
1256
  const { writeTreeToFilesystem } = await import("@better-fullstack/template-generator/fs-writer");
827
1257
  const path = await import("node:path");
828
1258
  const projectName = sanitizePath(input.projectName);
829
- const projectDir = path.resolve(process.cwd(), projectName);
1259
+ const targetDir = input.targetDir ? sanitizePath(input.targetDir) : void 0;
1260
+ const projectDir = path.resolve(targetDir ?? process.cwd(), projectName);
830
1261
  const config = buildProjectConfig(input, { projectDir });
831
1262
  await (await import("node:fs/promises")).mkdir(projectDir, { recursive: true });
832
1263
  const result = await generateVirtualProject({
@@ -848,21 +1279,25 @@ async function startMcpServer() {
848
1279
  const graphPreview = getMcpGraphPreview(config);
849
1280
  let addonWarnings = [];
850
1281
  if (config.addons.length > 0 && config.addons[0] !== "none") {
851
- const { setupAddons } = await import("./addons-setup-CV5uZyhH.mjs");
1282
+ const { setupAddons } = await import("./addons-setup-CSmj5ClW.mjs");
852
1283
  addonWarnings = await setupAddons(config);
853
1284
  }
854
1285
  const installCmd = getInstallCommand(input.ecosystem ?? "typescript", projectName, input.packageManager, input.javaBuildTool, input.javaWebFramework);
855
- return { content: [{
856
- type: "text",
857
- text: JSON.stringify({
858
- success: true,
859
- projectDirectory: projectDir,
860
- fileCount: result.tree.fileCount,
861
- ...graphPreview,
862
- ...addonWarnings.length > 0 ? { addonWarnings } : {},
863
- message: `Project created at ${projectDir}. Tell the user to run: ${installCmd}`
864
- }, null, 2)
865
- }] };
1286
+ const payload = {
1287
+ success: true,
1288
+ projectDirectory: projectDir,
1289
+ fileCount: result.tree.fileCount,
1290
+ ...graphPreview,
1291
+ ...addonWarnings.length > 0 ? { addonWarnings } : {},
1292
+ message: `Project created at ${projectDir}. Tell the user to run: ${installCmd}`
1293
+ };
1294
+ return {
1295
+ content: [{
1296
+ type: "text",
1297
+ text: JSON.stringify(payload, null, 2)
1298
+ }],
1299
+ structuredContent: payload
1300
+ };
866
1301
  } catch (error) {
867
1302
  return {
868
1303
  content: [{
@@ -873,12 +1308,22 @@ async function startMcpServer() {
873
1308
  };
874
1309
  }
875
1310
  });
876
- registerTool("bfs_plan_addition", "Validates what would be added to an existing project. Reads the project config (bts.jsonc) and checks which addons are new.", mcpInputSchema({
877
- projectDir: z.string().describe("Absolute path to the existing project directory"),
878
- addons: z.array(AddonsSchema).optional().describe("Addons to add"),
879
- webDeploy: WebDeploySchema.optional().describe("Web deployment option"),
880
- serverDeploy: ServerDeploySchema.optional().describe("Server deployment option")
881
- }), async ({ projectDir, addons, webDeploy, serverDeploy }) => {
1311
+ registerTool("bfs_plan_addition", {
1312
+ description: "Validates what would be added to an existing project. Reads the project config (bts.jsonc) and checks which addons are new.",
1313
+ inputSchema: mcpInputSchema({
1314
+ projectDir: z.string().describe("Absolute path to the existing project directory"),
1315
+ addons: z.array(AddonsSchema).optional().describe("Addons to add"),
1316
+ webDeploy: WebDeploySchema.optional().describe("Web deployment option"),
1317
+ serverDeploy: ServerDeploySchema.optional().describe("Server deployment option")
1318
+ }),
1319
+ outputSchema: planAdditionOutputSchema,
1320
+ annotations: {
1321
+ title: "Plan feature addition",
1322
+ readOnlyHint: true,
1323
+ idempotentHint: true,
1324
+ openWorldHint: false
1325
+ }
1326
+ }, async ({ projectDir, addons, webDeploy, serverDeploy }) => {
882
1327
  try {
883
1328
  const safePath = sanitizePath(projectDir);
884
1329
  const config = await readBtsConfig(safePath);
@@ -909,31 +1354,35 @@ async function startMcpServer() {
909
1354
  serverDeploy: serverDeploy ?? config.serverDeploy
910
1355
  }));
911
1356
  const compatibilityWarnings = compatResult.changes.length > 0 ? compatResult.changes.map((c) => c.message) : void 0;
912
- return { content: [{
913
- type: "text",
914
- text: JSON.stringify({
915
- success: true,
916
- existingConfig: {
917
- ecosystem: config.ecosystem,
918
- frontend: config.frontend,
919
- backend: config.backend,
920
- addons: config.addons,
921
- graphSummary: existingGraphPreview.graphSummary,
922
- effectiveStack: existingGraphPreview.effectiveStack,
923
- stackPartSpecs: existingGraphPreview.stackPartSpecs
924
- },
925
- proposedAdditions: {
926
- newAddons,
927
- webDeploy: webDeploy ?? null,
928
- serverDeploy: serverDeploy ?? null,
929
- graphSummary: proposedGraphPreview.graphSummary,
930
- effectiveStack: proposedGraphPreview.effectiveStack,
931
- stackPartSpecs: proposedGraphPreview.stackPartSpecs
932
- },
933
- alreadyPresent: (addons ?? []).filter((a) => existingAddons.has(a)),
934
- ...compatibilityWarnings ? { compatibilityWarnings } : {}
935
- }, null, 2)
936
- }] };
1357
+ const payload = {
1358
+ success: true,
1359
+ existingConfig: {
1360
+ ecosystem: config.ecosystem,
1361
+ frontend: config.frontend,
1362
+ backend: config.backend,
1363
+ addons: config.addons,
1364
+ graphSummary: existingGraphPreview.graphSummary,
1365
+ effectiveStack: existingGraphPreview.effectiveStack,
1366
+ stackPartSpecs: existingGraphPreview.stackPartSpecs
1367
+ },
1368
+ proposedAdditions: {
1369
+ newAddons,
1370
+ webDeploy: webDeploy ?? null,
1371
+ serverDeploy: serverDeploy ?? null,
1372
+ graphSummary: proposedGraphPreview.graphSummary,
1373
+ effectiveStack: proposedGraphPreview.effectiveStack,
1374
+ stackPartSpecs: proposedGraphPreview.stackPartSpecs
1375
+ },
1376
+ alreadyPresent: (addons ?? []).filter((a) => existingAddons.has(a)),
1377
+ ...compatibilityWarnings ? { compatibilityWarnings } : {}
1378
+ };
1379
+ return {
1380
+ content: [{
1381
+ type: "text",
1382
+ text: JSON.stringify(payload, null, 2)
1383
+ }],
1384
+ structuredContent: payload
1385
+ };
937
1386
  } catch (error) {
938
1387
  return {
939
1388
  content: [{
@@ -944,13 +1393,24 @@ async function startMcpServer() {
944
1393
  };
945
1394
  }
946
1395
  });
947
- registerTool("bfs_add_feature", "Adds addons/features to an existing Better-Fullstack project. Dependencies are NOT installed. Call bfs_plan_addition first to validate.", mcpInputSchema({
948
- projectDir: z.string().describe("Absolute path to the existing project directory"),
949
- addons: z.array(AddonsSchema).optional().describe("Addons to add"),
950
- webDeploy: WebDeploySchema.optional().describe("Web deployment option"),
951
- serverDeploy: ServerDeploySchema.optional().describe("Server deployment option"),
952
- packageManager: PackageManagerSchema.optional().describe("Package manager to use")
953
- }), async (input) => {
1396
+ registerTool("bfs_add_feature", {
1397
+ description: "Adds addons/features to an existing Better-Fullstack project. Dependencies are NOT installed. Call bfs_plan_addition first to validate.",
1398
+ inputSchema: mcpInputSchema({
1399
+ projectDir: z.string().describe("Absolute path to the existing project directory"),
1400
+ addons: z.array(AddonsSchema).optional().describe("Addons to add"),
1401
+ webDeploy: WebDeploySchema.optional().describe("Web deployment option"),
1402
+ serverDeploy: ServerDeploySchema.optional().describe("Server deployment option"),
1403
+ packageManager: PackageManagerSchema.optional().describe("Package manager to use")
1404
+ }),
1405
+ outputSchema: addFeatureOutputSchema,
1406
+ annotations: {
1407
+ title: "Add feature",
1408
+ readOnlyHint: false,
1409
+ destructiveHint: false,
1410
+ idempotentHint: false,
1411
+ openWorldHint: false
1412
+ }
1413
+ }, async (input) => {
954
1414
  try {
955
1415
  const safePath = sanitizePath(input.projectDir);
956
1416
  const { add } = await import("./index.mjs");
@@ -966,16 +1426,20 @@ async function startMcpServer() {
966
1426
  const existingConfig = await readBtsConfig(safePath);
967
1427
  const graphPreview = existingConfig ? getMcpGraphPreview(existingConfig) : void 0;
968
1428
  const installCmd = getInstallCommand(existingConfig?.ecosystem ?? "typescript", safePath.split("/").pop() ?? "project", input.packageManager, existingConfig?.javaBuildTool, existingConfig?.javaWebFramework);
969
- return { content: [{
970
- type: "text",
971
- text: JSON.stringify({
972
- success: true,
973
- addedAddons: result.addedAddons,
974
- projectDir: result.projectDir,
975
- ...graphPreview,
976
- message: `Added ${result.addedAddons.join(", ")} to project. Tell the user to run: ${installCmd}`
977
- }, null, 2)
978
- }] };
1429
+ const payload = {
1430
+ success: true,
1431
+ addedAddons: result.addedAddons,
1432
+ projectDir: result.projectDir,
1433
+ ...graphPreview,
1434
+ message: `Added ${result.addedAddons.join(", ")} to project. Tell the user to run: ${installCmd}`
1435
+ };
1436
+ return {
1437
+ content: [{
1438
+ type: "text",
1439
+ text: JSON.stringify(payload, null, 2)
1440
+ }],
1441
+ structuredContent: payload
1442
+ };
979
1443
  }
980
1444
  return {
981
1445
  content: [{