flutterflow-mcp 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/build/index.js +24 -0
  2. package/build/prompts/generate-page.js +6 -5
  3. package/build/prompts/modify-component.js +6 -5
  4. package/build/tools/find-component-usages.js +1 -10
  5. package/build/tools/find-page-navigations.js +1 -9
  6. package/build/tools/get-api-endpoints.d.ts +2 -0
  7. package/build/tools/get-api-endpoints.js +126 -0
  8. package/build/tools/get-app-settings.d.ts +2 -0
  9. package/build/tools/get-app-settings.js +169 -0
  10. package/build/tools/get-app-state.d.ts +2 -0
  11. package/build/tools/get-app-state.js +96 -0
  12. package/build/tools/get-custom-code.d.ts +2 -0
  13. package/build/tools/get-custom-code.js +380 -0
  14. package/build/tools/get-data-models.d.ts +2 -0
  15. package/build/tools/get-data-models.js +266 -0
  16. package/build/tools/get-editing-guide.d.ts +7 -0
  17. package/build/tools/get-editing-guide.js +185 -0
  18. package/build/tools/get-general-settings.d.ts +2 -0
  19. package/build/tools/get-general-settings.js +116 -0
  20. package/build/tools/get-in-app-purchases.d.ts +2 -0
  21. package/build/tools/get-in-app-purchases.js +51 -0
  22. package/build/tools/get-integrations.d.ts +2 -0
  23. package/build/tools/get-integrations.js +137 -0
  24. package/build/tools/get-page-summary.js +1 -18
  25. package/build/tools/get-project-setup.d.ts +2 -0
  26. package/build/tools/get-project-setup.js +212 -0
  27. package/build/tools/get-theme.d.ts +2 -0
  28. package/build/tools/get-theme.js +199 -0
  29. package/build/tools/get-yaml-docs.js +1 -111
  30. package/build/tools/list-files.js +22 -3
  31. package/build/tools/search-project-files.d.ts +2 -0
  32. package/build/tools/search-project-files.js +69 -0
  33. package/build/tools/sync-project.js +4 -1
  34. package/build/tools/update-yaml.js +1 -1
  35. package/build/tools/validate-yaml.js +19 -2
  36. package/build/utils/batch-process.d.ts +2 -0
  37. package/build/utils/batch-process.js +10 -0
  38. package/build/utils/cache.d.ts +5 -0
  39. package/build/utils/cache.js +16 -1
  40. package/build/utils/resolve-data-type.d.ts +2 -0
  41. package/build/utils/resolve-data-type.js +18 -0
  42. package/build/utils/topic-map.d.ts +7 -0
  43. package/build/utils/topic-map.js +115 -0
  44. package/docs/ff-yaml/00-overview.md +30 -1
  45. package/docs/ff-yaml/01-project-files.md +1380 -4
  46. package/docs/ff-yaml/08-custom-code.md +77 -1
  47. package/docs/ff-yaml/09-theming.md +46 -0
  48. package/package.json +1 -1
@@ -0,0 +1,185 @@
1
+ import { z } from "zod";
2
+ import { TOPIC_MAP, readDoc } from "../utils/topic-map.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Keyword sets for edit-type detection
5
+ // ---------------------------------------------------------------------------
6
+ const ADD_WIDGET_KEYWORDS = ["add", "new", "insert", "append", "place", "put"];
7
+ const COMPONENT_KEYWORDS = [
8
+ "component",
9
+ "reusable",
10
+ "refactor",
11
+ "extract",
12
+ ];
13
+ const EDIT_KEYWORDS = [
14
+ "change",
15
+ "modify",
16
+ "update",
17
+ "edit",
18
+ "fix",
19
+ "set",
20
+ "remove",
21
+ "delete",
22
+ "hide",
23
+ "show",
24
+ "toggle",
25
+ "enable",
26
+ "disable",
27
+ "move",
28
+ "replace",
29
+ "rename",
30
+ "resize",
31
+ "restyle",
32
+ ];
33
+ const CONFIG_KEYWORDS = [
34
+ "configure",
35
+ "settings",
36
+ "config",
37
+ "theme",
38
+ "auth",
39
+ "permission",
40
+ "navigation",
41
+ "nav",
42
+ "environment",
43
+ ];
44
+ function detectEditType(words) {
45
+ const hasComponent = words.some((w) => COMPONENT_KEYWORDS.includes(w));
46
+ const hasAdd = words.some((w) => ADD_WIDGET_KEYWORDS.includes(w));
47
+ const hasConfig = words.some((w) => CONFIG_KEYWORDS.includes(w));
48
+ // Priority 1: component + (add/create/refactor)
49
+ if (hasComponent &&
50
+ (hasAdd || words.includes("create") || words.includes("refactor"))) {
51
+ return "create-component";
52
+ }
53
+ // Priority 2: config keywords
54
+ if (hasConfig) {
55
+ return "configure-project";
56
+ }
57
+ // Priority 3: add widget keywords
58
+ if (hasAdd) {
59
+ return "add-widget";
60
+ }
61
+ // Default
62
+ return "edit-existing";
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // Workflow templates
66
+ // ---------------------------------------------------------------------------
67
+ function getWorkflow(editType, projectId) {
68
+ const pid = projectId ?? "projectId";
69
+ switch (editType) {
70
+ case "edit-existing":
71
+ return `## Workflow: Editing Existing Widgets
72
+
73
+ 1. \`list_pages(${pid})\` — find the page
74
+ 2. \`get_page_by_name(${pid}, "PageName")\` — read the page, find the widget key
75
+ 3. \`get_project_yaml(${pid}, "page/id-Scaffold_XXX/.../node/id-Widget_YYY")\` — fetch node-level YAML
76
+ 4. Modify the YAML (keep both \`inputValue\` and \`mostRecentInputValue\` in sync)
77
+ 5. \`validate_yaml(${pid}, fileKey, yaml)\` — validate before pushing
78
+ 6. \`update_project_yaml(${pid}, { fileKey: yaml })\` — push changes`;
79
+ case "add-widget":
80
+ return `## Workflow: Adding New Widgets
81
+
82
+ 1. \`list_pages(${pid})\` — find the page
83
+ 2. \`get_page_by_name(${pid}, "PageName")\` — read the page structure
84
+ 3. Construct three types of files:
85
+ - Widget tree outline (\`page/id-Scaffold_XXX/page-widget-tree-outline\`)
86
+ - Parent node file (Column, Row, etc.)
87
+ - Individual child node files for each new widget
88
+ 4. \`validate_yaml\` for each file
89
+ 5. \`update_project_yaml(${pid}, { ...allFiles })\` — push ALL files in one call`;
90
+ case "create-component":
91
+ return `## Workflow: Creating/Refactoring Components
92
+
93
+ 1. Read the existing page/component with \`get_page_by_name\` or \`get_component_summary\`
94
+ 2. Construct component files:
95
+ - Component metadata (\`component/id-Container_XXX\`)
96
+ - Widget tree outline (\`component/id-Container_XXX/component-widget-tree-outline\`)
97
+ - Root Container node with \`isDummyRoot: true\`
98
+ - Individual child node files
99
+ 3. If refactoring: update the source page to reference the component via \`componentClassKeyRef\`
100
+ 4. \`validate_yaml\` for each file
101
+ 5. \`update_project_yaml(${pid}, { ...allFiles })\` — push ALL files in one call
102
+ 6. See \`get_yaml_docs(topic: "component")\` for full schema details`;
103
+ case "configure-project":
104
+ return `## Workflow: Configuring Project Settings
105
+
106
+ 1. \`sync_project(${pid})\` — sync the project cache
107
+ 2. Use cache tools to read current config:
108
+ - \`get_project_config\` — app details, auth, nav bar, permissions
109
+ - \`get_theme\` — colors, typography, breakpoints
110
+ - \`get_app_state\` — state variables, constants, environment
111
+ 3. \`get_project_yaml(${pid}, "fileName")\` — fetch the specific config file
112
+ 4. Modify the YAML
113
+ 5. \`validate_yaml\` then \`update_project_yaml\``;
114
+ }
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // Universal rules
118
+ // ---------------------------------------------------------------------------
119
+ const UNIVERSAL_RULES = `## Critical YAML Rules
120
+
121
+ - **Always update both \`inputValue\` AND \`mostRecentInputValue\`** to the same value — they must stay in sync.
122
+ - **Exceptions:** \`fontWeightValue\` and \`fontSizeValue\` only accept \`inputValue\` (no \`mostRecentInputValue\`).
123
+ - **Use node-level file keys** for targeted edits, not the full page YAML.
124
+ - **Always validate before pushing** — call \`validate_yaml\` before \`update_project_yaml\`.
125
+ - **Adding widgets requires node-level files** — push the tree outline + individual nodes together.
126
+ - **Do NOT guess YAML field names** — use \`get_yaml_docs(topic: "...")\` to look them up.`;
127
+ // ---------------------------------------------------------------------------
128
+ // Doc resolution
129
+ // ---------------------------------------------------------------------------
130
+ function resolveDocFiles(words) {
131
+ const seen = new Set();
132
+ const files = [];
133
+ for (const word of words) {
134
+ // Normalize: lowercase, strip dashes/underscores
135
+ const key = word.toLowerCase().replace(/[\s_-]+/g, "");
136
+ const docFile = TOPIC_MAP[key];
137
+ if (docFile && !seen.has(docFile)) {
138
+ seen.add(docFile);
139
+ files.push(docFile);
140
+ }
141
+ }
142
+ return files;
143
+ }
144
+ // ---------------------------------------------------------------------------
145
+ // Tool registration
146
+ // ---------------------------------------------------------------------------
147
+ export function registerGetEditingGuideTool(server) {
148
+ server.tool("get_editing_guide", "Get the recommended workflow and relevant documentation for a FlutterFlow editing task. Call this BEFORE modifying any YAML. Describe what you want to do (e.g. 'change button color', 'add a TextField to the login page', 'create a reusable header component') and receive the correct workflow steps, YAML schemas, and critical rules.", {
149
+ task: z
150
+ .string()
151
+ .describe("Natural language description of what you want to do"),
152
+ projectId: z
153
+ .string()
154
+ .optional()
155
+ .describe("Optional project ID to include in workflow steps"),
156
+ }, async ({ task, projectId }) => {
157
+ // Tokenize into lowercase words
158
+ const words = task.toLowerCase().split(/\W+/).filter(Boolean);
159
+ // Detect edit type
160
+ const editType = detectEditType(words);
161
+ // Build workflow section
162
+ const workflow = getWorkflow(editType, projectId);
163
+ // Resolve matching doc files
164
+ const docFiles = resolveDocFiles(words);
165
+ // Read matched docs
166
+ const docSections = [];
167
+ for (const file of docFiles) {
168
+ const content = readDoc(file);
169
+ if (content) {
170
+ docSections.push(`---\n\n# Reference: ${file}\n\n${content}`);
171
+ }
172
+ }
173
+ // Assemble response
174
+ const parts = [workflow, "", UNIVERSAL_RULES, ""];
175
+ if (docSections.length > 0) {
176
+ parts.push(...docSections);
177
+ }
178
+ else {
179
+ parts.push('No specific widget/topic docs matched your task. Use `get_yaml_docs(topic: "...")` to search for relevant schemas, or `get_yaml_docs()` for the full index.');
180
+ }
181
+ return {
182
+ content: [{ type: "text", text: parts.join("\n") }],
183
+ };
184
+ });
185
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerGetGeneralSettingsTool(server: McpServer): void;
@@ -0,0 +1,116 @@
1
+ import { z } from "zod";
2
+ import YAML from "yaml";
3
+ import { cacheRead, cacheMeta } from "../utils/cache.js";
4
+ async function resolvePageName(projectId, scaffoldId) {
5
+ const content = await cacheRead(projectId, `page/id-${scaffoldId}`);
6
+ if (!content)
7
+ return scaffoldId;
8
+ const nameMatch = content.match(/^name:\s*(.+)$/m);
9
+ return nameMatch ? nameMatch[1].trim() : scaffoldId;
10
+ }
11
+ export function registerGetGeneralSettingsTool(server) {
12
+ server.tool("get_general_settings", "Get General settings — App Details (name, package, initial page, routing), App Assets (icon, splash, error image), Nav Bar & App Bar. Mirrors the FlutterFlow 'General' settings section. Cache-based, no API calls. Run sync_project first.", {
13
+ projectId: z.string().describe("The FlutterFlow project ID"),
14
+ }, async ({ projectId }) => {
15
+ const meta = await cacheMeta(projectId);
16
+ if (!meta) {
17
+ return {
18
+ content: [
19
+ {
20
+ type: "text",
21
+ text: `No cache found for project "${projectId}". Run sync_project first.`,
22
+ },
23
+ ],
24
+ };
25
+ }
26
+ const [appDetailsRaw, appAssetsRaw, navBarRaw] = await Promise.all([
27
+ cacheRead(projectId, "app-details"),
28
+ cacheRead(projectId, "app-assets"),
29
+ cacheRead(projectId, "nav-bar"),
30
+ ]);
31
+ const sections = ["# General Settings"];
32
+ // --- App Details ---
33
+ if (appDetailsRaw) {
34
+ const appDetails = YAML.parse(appDetailsRaw);
35
+ const appName = appDetails.name || "Unknown";
36
+ // Package name: take first env's packageName
37
+ let packageName = "not set";
38
+ const allAppNames = appDetails.allAppNames;
39
+ const appNames = allAppNames?.appNames;
40
+ if (appNames) {
41
+ const firstEnvKey = Object.keys(appNames)[0];
42
+ if (firstEnvKey) {
43
+ packageName = appNames[firstEnvKey].packageName || "not set";
44
+ }
45
+ }
46
+ // Initial page
47
+ const initialPageKey = appDetails.initialPageKeyRef?.key;
48
+ let initialPageLine = "not set";
49
+ if (initialPageKey) {
50
+ const name = await resolvePageName(projectId, initialPageKey);
51
+ initialPageLine = `${name} (${initialPageKey})`;
52
+ }
53
+ // Routing
54
+ const routing = appDetails.routingSettings;
55
+ const routingEnabled = routing?.enableRouting === true;
56
+ const subroutes = routing?.pagesAreSubroutesOfRoot === true;
57
+ sections.push(`\n## App Details`, `Name: ${appName}`, `Package name: ${packageName}`, `Initial page: ${initialPageLine}`, `Routing: ${routingEnabled ? "enabled" : "disabled"}, pages are subroutes: ${subroutes ? "yes" : "no"}`);
58
+ }
59
+ else {
60
+ sections.push(`\n## App Details`, `(not configured)`);
61
+ }
62
+ // --- App Assets ---
63
+ if (appAssetsRaw) {
64
+ const appAssets = YAML.parse(appAssetsRaw);
65
+ const appIconPath = appAssets.appIconPath || "not set";
66
+ const splashImage = appAssets.splashImage;
67
+ const errorImagePath = appAssets.errorImagePath || "not set";
68
+ sections.push(`\n## App Assets`);
69
+ sections.push(`App icon: ${appIconPath}`);
70
+ if (splashImage) {
71
+ const splashPath = splashImage.path || "not set";
72
+ const splashFit = splashImage.fit || "not set";
73
+ const splashDuration = splashImage.minSplashScreenDuration;
74
+ sections.push(`Splash image: ${splashPath} (fit: ${splashFit})`);
75
+ if (splashDuration)
76
+ sections.push(`Splash duration: ${splashDuration}ms`);
77
+ }
78
+ else {
79
+ sections.push(`Splash image: not set`);
80
+ }
81
+ sections.push(`Error image: ${errorImagePath}`);
82
+ }
83
+ else {
84
+ sections.push(`\n## App Assets`, `(not configured)`);
85
+ }
86
+ // --- Nav Bar & App Bar ---
87
+ if (navBarRaw) {
88
+ const navBar = YAML.parse(navBarRaw);
89
+ const visible = navBar.show === true;
90
+ if (visible) {
91
+ const navType = navBar.navBarType || "UNKNOWN";
92
+ const labels = navBar.labels === true;
93
+ const pageRefs = navBar.pageKeyRefOrder;
94
+ sections.push(`\n## Nav Bar & App Bar`, `Visible: Yes`, `Type: ${navType}`, `Labels: ${labels ? "Yes" : "No"}`);
95
+ if (pageRefs && pageRefs.length > 0) {
96
+ const tabs = [];
97
+ for (let i = 0; i < pageRefs.length; i++) {
98
+ const scaffoldId = pageRefs[i].key;
99
+ const name = await resolvePageName(projectId, scaffoldId);
100
+ tabs.push(` ${i + 1}. ${name} (${scaffoldId})`);
101
+ }
102
+ sections.push(`Tabs:`, ...tabs);
103
+ }
104
+ }
105
+ else {
106
+ sections.push(`\n## Nav Bar & App Bar`, `Visible: No`);
107
+ }
108
+ }
109
+ else {
110
+ sections.push(`\n## Nav Bar & App Bar`, `(not configured)`);
111
+ }
112
+ return {
113
+ content: [{ type: "text", text: sections.join("\n") }],
114
+ };
115
+ });
116
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerGetInAppPurchasesTool(server: McpServer): void;
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+ import YAML from "yaml";
3
+ import { cacheRead, cacheMeta } from "../utils/cache.js";
4
+ const PROVIDERS = [
5
+ { key: "stripe", label: "Stripe" },
6
+ { key: "braintree", label: "Braintree" },
7
+ { key: "revenue-cat", label: "RevenueCat" },
8
+ { key: "razorpay", label: "Razorpay" },
9
+ ];
10
+ export function registerGetInAppPurchasesTool(server) {
11
+ server.tool("get_in_app_purchases", "Get In-App Purchases & Subscriptions settings — Stripe, Braintree, RevenueCat, Razorpay. Mirrors the FlutterFlow 'In App Purchases & Subscriptions' section. Cache-based, no API calls. Run sync_project first.", {
12
+ projectId: z.string().describe("The FlutterFlow project ID"),
13
+ }, async ({ projectId }) => {
14
+ const meta = await cacheMeta(projectId);
15
+ if (!meta) {
16
+ return {
17
+ content: [
18
+ {
19
+ type: "text",
20
+ text: `No cache found for project "${projectId}". Run sync_project first.`,
21
+ },
22
+ ],
23
+ };
24
+ }
25
+ const sections = ["# In-App Purchases & Subscriptions"];
26
+ for (const provider of PROVIDERS) {
27
+ const raw = await cacheRead(projectId, provider.key);
28
+ sections.push(`\n## ${provider.label}`);
29
+ if (!raw) {
30
+ sections.push("(not configured)");
31
+ continue;
32
+ }
33
+ const doc = YAML.parse(raw);
34
+ const enabled = doc.enabled === true;
35
+ sections.push(`Enabled: ${enabled ? "Yes" : "No"}`);
36
+ // Show any additional top-level scalar fields (skip 'enabled' itself and complex objects)
37
+ for (const [key, value] of Object.entries(doc)) {
38
+ if (key === "enabled")
39
+ continue;
40
+ if (value === null || value === undefined)
41
+ continue;
42
+ if (typeof value === "object")
43
+ continue;
44
+ sections.push(`${key}: ${String(value)}`);
45
+ }
46
+ }
47
+ return {
48
+ content: [{ type: "text", text: sections.join("\n") }],
49
+ };
50
+ });
51
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerGetIntegrationsTool(server: McpServer): void;
@@ -0,0 +1,137 @@
1
+ import { z } from "zod";
2
+ import YAML from "yaml";
3
+ import { cacheRead, cacheMeta } from "../utils/cache.js";
4
+ /**
5
+ * Fields that should never be output — API keys, secrets, tokens, etc.
6
+ * Only IDs and non-sensitive configuration are shown.
7
+ */
8
+ const SENSITIVE_KEYS = new Set([
9
+ "searchApiKey",
10
+ "supabaseAnonKey",
11
+ "supabaseServiceRoleKey",
12
+ "apiKey",
13
+ "secretKey",
14
+ "secret",
15
+ "token",
16
+ "accessToken",
17
+ "refreshToken",
18
+ "privateKey",
19
+ ]);
20
+ // ---------------------------------------------------------------------------
21
+ // Custom formatters for specific integrations
22
+ // ---------------------------------------------------------------------------
23
+ function formatAlgolia(doc) {
24
+ const lines = [];
25
+ const enabled = doc.enabled === true;
26
+ lines.push(`Enabled: ${enabled ? "Yes" : "No"}`);
27
+ const appId = doc.applicationId || "not set";
28
+ lines.push(`Application ID: ${appId}`);
29
+ const collections = doc.indexedCollections;
30
+ if (collections && collections.length > 0) {
31
+ lines.push(`Indexed collections:`);
32
+ for (const col of collections) {
33
+ lines.push(` - ${col.name || "unnamed"}`);
34
+ }
35
+ }
36
+ else {
37
+ lines.push(`Indexed collections: none`);
38
+ }
39
+ return lines;
40
+ }
41
+ function formatGoogleMaps(doc) {
42
+ const androidKey = doc.androidKey || "not set";
43
+ const iosKey = doc.iosKey || "not set";
44
+ const webKey = doc.webKey || "not set";
45
+ return [
46
+ `Android key: ${androidKey}`,
47
+ `iOS key: ${iosKey}`,
48
+ `Web key: ${webKey}`,
49
+ ];
50
+ }
51
+ function formatFirebaseAnalytics(doc) {
52
+ const lines = [];
53
+ const enabled = doc.enabled === true;
54
+ lines.push(`Enabled: ${enabled ? "Yes" : "No"}`);
55
+ const settings = doc.automaticEventSettings;
56
+ if (settings) {
57
+ lines.push(`Automatic event settings:`);
58
+ lines.push(` onPageLoad: ${settings.onPageLoad === true ? "Yes" : "No"}`);
59
+ lines.push(` onActionsStart: ${settings.onActionsStart === true ? "Yes" : "No"}`);
60
+ lines.push(` onAuth: ${settings.onAuth === true ? "Yes" : "No"}`);
61
+ }
62
+ return lines;
63
+ }
64
+ /**
65
+ * Generic formatter: shows `enabled` status and all non-sensitive
66
+ * top-level string/boolean fields.
67
+ */
68
+ function formatGeneric(doc) {
69
+ const lines = [];
70
+ if ("enabled" in doc) {
71
+ lines.push(`Enabled: ${doc.enabled === true ? "Yes" : "No"}`);
72
+ }
73
+ for (const [key, value] of Object.entries(doc)) {
74
+ if (key === "enabled")
75
+ continue;
76
+ if (SENSITIVE_KEYS.has(key))
77
+ continue;
78
+ if (typeof value === "string" || typeof value === "boolean") {
79
+ lines.push(`${key}: ${typeof value === "boolean" ? (value ? "Yes" : "No") : value}`);
80
+ }
81
+ }
82
+ return lines;
83
+ }
84
+ // ---------------------------------------------------------------------------
85
+ // Integration list — order matches the spec
86
+ // ---------------------------------------------------------------------------
87
+ const INTEGRATIONS = [
88
+ { fileKey: "supabase", displayName: "Supabase" },
89
+ { fileKey: "sqlite", displayName: "SQLite" },
90
+ { fileKey: "github", displayName: "GitHub" },
91
+ { fileKey: "algolia", displayName: "Algolia", format: formatAlgolia },
92
+ { fileKey: "firebase-analytics", displayName: "Google Analytics", format: formatFirebaseAnalytics },
93
+ { fileKey: "google-maps", displayName: "Google Maps", format: formatGoogleMaps },
94
+ { fileKey: "admob", displayName: "AdMob" },
95
+ { fileKey: "mux", displayName: "Mux Livestream" },
96
+ { fileKey: "onesignal", displayName: "OneSignal" },
97
+ { fileKey: "gemini", displayName: "Gemini" },
98
+ ];
99
+ // ---------------------------------------------------------------------------
100
+ // Tool registration
101
+ // ---------------------------------------------------------------------------
102
+ export function registerGetIntegrationsTool(server) {
103
+ server.tool("get_integrations", "Get Integrations settings — Supabase, SQLite, GitHub, Algolia, Google Analytics, Google Maps, AdMob, Mux Livestream, OneSignal, Gemini. Mirrors the FlutterFlow 'Integrations' section. Cache-based, no API calls. Run sync_project first.", {
104
+ projectId: z.string().describe("The FlutterFlow project ID"),
105
+ }, async ({ projectId }) => {
106
+ const meta = await cacheMeta(projectId);
107
+ if (!meta) {
108
+ return {
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: `No cache found for project "${projectId}". Run sync_project first.`,
113
+ },
114
+ ],
115
+ };
116
+ }
117
+ // Read all integration files in parallel
118
+ const rawResults = await Promise.all(INTEGRATIONS.map((def) => cacheRead(projectId, def.fileKey)));
119
+ const sections = ["# Integrations"];
120
+ for (let i = 0; i < INTEGRATIONS.length; i++) {
121
+ const def = INTEGRATIONS[i];
122
+ const raw = rawResults[i];
123
+ sections.push(`\n## ${def.displayName}`);
124
+ if (!raw) {
125
+ sections.push("(not configured)");
126
+ continue;
127
+ }
128
+ const doc = YAML.parse(raw);
129
+ const formatter = def.format || formatGeneric;
130
+ const lines = formatter(doc);
131
+ sections.push(lines.join("\n"));
132
+ }
133
+ return {
134
+ content: [{ type: "text", text: sections.join("\n") }],
135
+ };
136
+ });
137
+ }
@@ -6,6 +6,7 @@ import { parseTreeOutline } from "../utils/page-summary/tree-walker.js";
6
6
  import { extractNodeInfo } from "../utils/page-summary/node-extractor.js";
7
7
  import { summarizeTriggers } from "../utils/page-summary/action-summarizer.js";
8
8
  import { formatPageSummary } from "../utils/page-summary/formatter.js";
9
+ import { resolveDataType } from "../utils/resolve-data-type.js";
9
10
  /**
10
11
  * Resolve a page name or scaffold ID to its cache file key.
11
12
  * Returns all available page names if no match found.
@@ -41,24 +42,6 @@ export async function resolvePage(projectId, pageName, scaffoldId) {
41
42
  // ---------------------------------------------------------------------------
42
43
  // Metadata extraction
43
44
  // ---------------------------------------------------------------------------
44
- /** Resolve a scalar/list data type to a readable string. */
45
- function resolveDataType(dt) {
46
- if (dt.listType) {
47
- const inner = dt.listType;
48
- return `List<${inner.scalarType || "unknown"}>`;
49
- }
50
- if (dt.scalarType === "DataStruct") {
51
- const sub = dt.subType;
52
- const dsi = sub?.dataStructIdentifier;
53
- return dsi?.name ? `DataStruct:${dsi.name}` : "DataStruct";
54
- }
55
- if (dt.enumType) {
56
- const en = dt.enumType;
57
- const eid = en.enumIdentifier;
58
- return eid?.name ? `Enum:${eid.name}` : "Enum";
59
- }
60
- return dt.scalarType || "unknown";
61
- }
62
45
  /** Extract page metadata (name, params, state) from the top-level page YAML. */
63
46
  async function extractPageMeta(projectId, page, folder) {
64
47
  const content = await cacheRead(projectId, page.pageFileKey);
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerGetProjectSetupTool(server: McpServer): void;