flutterflow-mcp 0.2.5 → 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.
package/build/index.js CHANGED
@@ -25,8 +25,14 @@ import { registerGetAppStateTool } from "./tools/get-app-state.js";
25
25
  import { registerGetApiEndpointsTool } from "./tools/get-api-endpoints.js";
26
26
  import { registerGetDataModelsTool } from "./tools/get-data-models.js";
27
27
  import { registerGetCustomCodeTool } from "./tools/get-custom-code.js";
28
- import { registerGetProjectConfigTool } from "./tools/get-project-config.js";
29
28
  import { registerGetThemeTool } from "./tools/get-theme.js";
29
+ import { registerGetEditingGuideTool } from "./tools/get-editing-guide.js";
30
+ import { registerSearchProjectFilesTool } from "./tools/search-project-files.js";
31
+ import { registerGetGeneralSettingsTool } from "./tools/get-general-settings.js";
32
+ import { registerGetProjectSetupTool } from "./tools/get-project-setup.js";
33
+ import { registerGetAppSettingsTool } from "./tools/get-app-settings.js";
34
+ import { registerGetInAppPurchasesTool } from "./tools/get-in-app-purchases.js";
35
+ import { registerGetIntegrationsTool } from "./tools/get-integrations.js";
30
36
  const server = new McpServer({
31
37
  name: "ff-mcp",
32
38
  version: "0.1.0",
@@ -50,8 +56,14 @@ registerGetAppStateTool(server);
50
56
  registerGetApiEndpointsTool(server);
51
57
  registerGetDataModelsTool(server);
52
58
  registerGetCustomCodeTool(server);
53
- registerGetProjectConfigTool(server);
54
59
  registerGetThemeTool(server);
60
+ registerGetEditingGuideTool(server);
61
+ registerSearchProjectFilesTool(server);
62
+ registerGetGeneralSettingsTool(server);
63
+ registerGetProjectSetupTool(server);
64
+ registerGetAppSettingsTool(server);
65
+ registerGetInAppPurchasesTool(server);
66
+ registerGetIntegrationsTool(server);
55
67
  // Register resources
56
68
  registerResources(server, client);
57
69
  registerDocsResources(server);
@@ -16,14 +16,15 @@ export function registerGeneratePagePrompt(server) {
16
16
  ## Instructions
17
17
 
18
18
  1. First, use the list_project_files tool with projectId "${projectId}" to understand the existing project structure.
19
- 2. Then, use get_project_yaml to read a few existing pages to understand the YAML schema and conventions used in this project.
20
- 3. Based on the following description, generate valid FlutterFlow YAML for a new page:
19
+ 2. Use \`get_editing_guide\` with a description of the page you want to create to get the correct YAML schemas, workflow steps, and critical rules.
20
+ 3. Then, use get_project_yaml to read a few existing pages to understand the YAML schema and conventions used in this project.
21
+ 4. Based on the following description, generate valid FlutterFlow YAML for a new page:
21
22
 
22
23
  **Page Description:** ${description}
23
24
 
24
- 4. Use validate_yaml to check your generated YAML is valid.
25
- 5. If validation passes, use update_project_yaml to push the new page to the project.
26
- 6. If validation fails, fix the errors and try again.
25
+ 5. Use validate_yaml to check your generated YAML is valid.
26
+ 6. If validation passes, use update_project_yaml to push the new page to the project.
27
+ 7. If validation fails, fix the errors and try again.
27
28
 
28
29
  ## Important
29
30
  - Follow the exact YAML structure you observed in existing pages.
@@ -19,14 +19,15 @@ export function registerModifyComponentPrompt(server) {
19
19
  ## Instructions
20
20
 
21
21
  1. Use get_project_yaml with projectId "${projectId}" and fileName "${fileName}" to read the current component YAML.
22
- 2. Understand the current structure and widget tree.
23
- 3. Apply the following changes:
22
+ 2. Use \`get_editing_guide\` with a description of the changes you want to make to get the correct YAML schemas and critical rules.
23
+ 3. Understand the current structure and widget tree.
24
+ 4. Apply the following changes:
24
25
 
25
26
  **Requested Changes:** ${changes}
26
27
 
27
- 4. Use validate_yaml to verify your modified YAML is valid.
28
- 5. If validation passes, use update_project_yaml to push the changes.
29
- 6. If validation fails, fix the errors and try again.
28
+ 5. Use validate_yaml to verify your modified YAML is valid.
29
+ 6. If validation passes, use update_project_yaml to push the changes.
30
+ 7. If validation fails, fix the errors and try again.
30
31
 
31
32
  ## Important
32
33
  - Preserve all existing structure you are not modifying.
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerGetAppSettingsTool(server: McpServer): void;
@@ -0,0 +1,169 @@
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 registerGetAppSettingsTool(server) {
12
+ server.tool("get_app_settings", "Get App Settings — Authentication, Push Notifications, Mobile Deployment (version, build, stores), Web Deployment (SEO, title). Mirrors the FlutterFlow 'App 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 [authRaw, appDetailsRaw, pushRaw, mobileRaw, webRaw] = await Promise.all([
27
+ cacheRead(projectId, "authentication"),
28
+ cacheRead(projectId, "app-details"),
29
+ cacheRead(projectId, "push-notifications"),
30
+ cacheRead(projectId, "mobile-deployment"),
31
+ cacheRead(projectId, "web-publishing"),
32
+ ]);
33
+ const sections = ["# App Settings"];
34
+ // --- Authentication ---
35
+ sections.push(`\n## Authentication`);
36
+ if (authRaw) {
37
+ const auth = YAML.parse(authRaw);
38
+ const active = auth.active === true;
39
+ if (active) {
40
+ let provider = "Unknown";
41
+ const firebaseConfigs = auth.firebaseConfigFileInfos;
42
+ const supabase = auth.supabase;
43
+ if (firebaseConfigs && firebaseConfigs.length > 0) {
44
+ provider = "Firebase";
45
+ }
46
+ else if (supabase && Object.keys(supabase).length > 0) {
47
+ provider = "Supabase";
48
+ }
49
+ sections.push(`Status: Active`, `Provider: ${provider}`);
50
+ }
51
+ else {
52
+ sections.push(`Status: Inactive`);
53
+ }
54
+ }
55
+ else {
56
+ sections.push(`Status: Inactive`);
57
+ }
58
+ // Auth page info from app-details
59
+ if (appDetailsRaw) {
60
+ const appDetails = YAML.parse(appDetailsRaw);
61
+ const authPageInfo = appDetails.authPageInfo;
62
+ if (authPageInfo) {
63
+ const homeRef = authPageInfo.homePageNodeKeyRef?.key;
64
+ const signInRef = authPageInfo.signInPageNodeKeyRef?.key;
65
+ if (homeRef) {
66
+ const name = await resolvePageName(projectId, homeRef);
67
+ sections.push(`Home page: ${name} (${homeRef})`);
68
+ }
69
+ if (signInRef) {
70
+ const name = await resolvePageName(projectId, signInRef);
71
+ sections.push(`Sign-in page: ${name} (${signInRef})`);
72
+ }
73
+ }
74
+ }
75
+ // --- Push Notifications ---
76
+ sections.push(`\n## Push Notifications`);
77
+ if (pushRaw) {
78
+ const push = YAML.parse(pushRaw);
79
+ const enabled = push.enabled === true;
80
+ const allowScheduled = push.allowScheduledNotifications === true;
81
+ const autoPrompt = push.autoPromptUsersForNotificationsPermission === true;
82
+ sections.push(`Enabled: ${enabled ? "Yes" : "No"}`, `Scheduled notifications: ${allowScheduled ? "Yes" : "No"}`, `Auto-prompt permission: ${autoPrompt ? "Yes" : "No"}`);
83
+ const lastNotif = push.lastNotificationSent;
84
+ if (lastNotif) {
85
+ const title = lastNotif.notificationTitle || "(untitled)";
86
+ const status = lastNotif.status || "unknown";
87
+ sections.push(`Last notification: "${title}" (${status})`);
88
+ }
89
+ }
90
+ else {
91
+ sections.push(`(not configured)`);
92
+ }
93
+ // --- Mobile Deployment ---
94
+ sections.push(`\n## Mobile Deployment`);
95
+ if (mobileRaw) {
96
+ const mobile = YAML.parse(mobileRaw);
97
+ const settingsMap = mobile.codemagicSettingsMap;
98
+ if (settingsMap && Object.keys(settingsMap).length > 0) {
99
+ // Prefer PROD, fall back to first key
100
+ const envKey = settingsMap.PROD
101
+ ? "PROD"
102
+ : Object.keys(settingsMap)[0];
103
+ const envSettings = settingsMap[envKey];
104
+ sections.push(`Environment: ${envKey}`);
105
+ // Build version
106
+ const buildVersion = envSettings.buildVersion;
107
+ if (buildVersion) {
108
+ if (buildVersion.buildVersion) {
109
+ sections.push(`Version: ${buildVersion.buildVersion}`);
110
+ }
111
+ if (buildVersion.buildNumber != null) {
112
+ sections.push(`Build number: ${buildVersion.buildNumber}`);
113
+ }
114
+ if (buildVersion.lastSubmitted) {
115
+ sections.push(`Last submitted: ${buildVersion.lastSubmitted}`);
116
+ }
117
+ }
118
+ // Play Store
119
+ const playStore = envSettings.playStoreSettings;
120
+ if (playStore?.playTrack) {
121
+ sections.push(`Play Store track: ${playStore.playTrack}`);
122
+ }
123
+ // App Store — NEVER output private keys
124
+ const appStore = envSettings.appStoreSettings;
125
+ if (appStore?.ascAppId) {
126
+ sections.push(`App Store ID: ${appStore.ascAppId}`);
127
+ }
128
+ }
129
+ else {
130
+ sections.push(`(not configured)`);
131
+ }
132
+ }
133
+ else {
134
+ sections.push(`(not configured)`);
135
+ }
136
+ // --- Web Deployment ---
137
+ sections.push(`\n## Web Deployment`);
138
+ if (webRaw) {
139
+ const web = YAML.parse(webRaw);
140
+ const webSettings = web.webSettings;
141
+ if (webSettings && Object.keys(webSettings).length > 0) {
142
+ // Prefer PROD, fall back to first key
143
+ const envKey = webSettings.PROD
144
+ ? "PROD"
145
+ : Object.keys(webSettings)[0];
146
+ const envData = webSettings[envKey];
147
+ sections.push(`Environment: ${envKey}`);
148
+ if (envData.pageTitle) {
149
+ sections.push(`Page title: ${envData.pageTitle}`);
150
+ }
151
+ if (envData.seoDescription) {
152
+ sections.push(`SEO description: ${envData.seoDescription}`);
153
+ }
154
+ if (envData.orientation) {
155
+ sections.push(`Orientation: ${envData.orientation}`);
156
+ }
157
+ }
158
+ else {
159
+ sections.push(`(not configured)`);
160
+ }
161
+ }
162
+ else {
163
+ sections.push(`(not configured)`);
164
+ }
165
+ return {
166
+ content: [{ type: "text", text: sections.join("\n") }],
167
+ };
168
+ });
169
+ }
@@ -82,6 +82,24 @@ function formatAgent(agent) {
82
82
  }
83
83
  return lines.join("\n");
84
84
  }
85
+ function formatAppAction(action) {
86
+ const lines = [];
87
+ lines.push(`### ${action.name}`);
88
+ lines.push(`Key: ${action.key} | File: \`${action.fileKey}\``);
89
+ lines.push(`Root action: ${action.rootActionType}(${action.rootActionName})`);
90
+ if (action.description) {
91
+ lines.push(`Description: "${action.description}"`);
92
+ }
93
+ return lines.join("\n");
94
+ }
95
+ function formatCustomFile(file) {
96
+ const lines = [];
97
+ lines.push(`### ${file.name}`);
98
+ lines.push(`Key: ${file.key} | File: \`${file.fileKey}\``);
99
+ lines.push(`Type: ${file.fileType}`);
100
+ lines.push(`Actions: ${file.initialCount} initial, ${file.finalCount} final`);
101
+ return lines.join("\n");
102
+ }
85
103
  function resolveReturnType(returnParam) {
86
104
  if (!returnParam)
87
105
  return null;
@@ -207,14 +225,80 @@ async function processAgents(projectId, nameFilter) {
207
225
  return { name: identifierName, key: idKey, fileKey, displayName, status, provider, model, requestTypes, responseType, description };
208
226
  }).then((results) => results.filter((r) => r !== null));
209
227
  }
228
+ async function processAppActions(projectId, nameFilter) {
229
+ const allKeys = await listCachedKeys(projectId, "app-action-components/id-");
230
+ const topKeys = allKeys.filter((k) => /^app-action-components\/id-[a-z0-9]+$/i.test(k));
231
+ return batchProcess(topKeys, 10, async (fileKey) => {
232
+ const content = await cacheRead(projectId, fileKey);
233
+ if (!content)
234
+ return null;
235
+ const doc = YAML.parse(content);
236
+ const id = doc.identifier;
237
+ const name = id?.name || "unknown";
238
+ if (nameFilter && name.toLowerCase() !== nameFilter.toLowerCase())
239
+ return null;
240
+ const idKey = id?.key || fileKey.match(/id-([a-z0-9]+)$/i)?.[1] || "unknown";
241
+ const description = doc.description || "";
242
+ // Extract root action type and name
243
+ let rootActionType = "unknown";
244
+ let rootActionName = "unknown";
245
+ const actions = doc.actions;
246
+ const rootAction = actions?.rootAction;
247
+ const actionObj = rootAction?.action;
248
+ if (actionObj) {
249
+ const actionKeys = Object.keys(actionObj);
250
+ if (actionKeys.length > 0) {
251
+ rootActionType = actionKeys[0];
252
+ const actionBody = actionObj[rootActionType];
253
+ if (actionBody) {
254
+ // Look for a sub-key ending in "Identifier" to get the name
255
+ for (const subKey of Object.keys(actionBody)) {
256
+ if (subKey.endsWith("Identifier")) {
257
+ const identifierObj = actionBody[subKey];
258
+ rootActionName = identifierObj?.name || rootActionName;
259
+ break;
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ return { name, key: idKey, fileKey, rootActionType, rootActionName, description };
266
+ }).then((results) => results.filter((r) => r !== null));
267
+ }
268
+ async function processCustomFiles(projectId, nameFilter) {
269
+ const allKeys = await listCachedKeys(projectId, "custom-file/id-");
270
+ const topKeys = allKeys.filter((k) => /^custom-file\/id-[^/]+$/i.test(k) && k !== "custom-file/id-MAIN");
271
+ return batchProcess(topKeys, 10, async (fileKey) => {
272
+ const content = await cacheRead(projectId, fileKey);
273
+ if (!content)
274
+ return null;
275
+ const doc = YAML.parse(content);
276
+ const keyMatch = fileKey.match(/^custom-file\/id-(.+)$/i);
277
+ const key = keyMatch?.[1] || "unknown";
278
+ const name = key;
279
+ if (nameFilter && name.toLowerCase() !== nameFilter.toLowerCase())
280
+ return null;
281
+ const fileType = doc.type || "UNKNOWN";
282
+ const actions = doc.actions || [];
283
+ let initialCount = 0;
284
+ let finalCount = 0;
285
+ for (const action of actions) {
286
+ if (action.type === "INITIAL_ACTION")
287
+ initialCount++;
288
+ else if (action.type === "FINAL_ACTION")
289
+ finalCount++;
290
+ }
291
+ return { name, key, fileKey, fileType, initialCount, finalCount };
292
+ }).then((results) => results.filter((r) => r !== null));
293
+ }
210
294
  // ---------------------------------------------------------------------------
211
295
  // Tool registration
212
296
  // ---------------------------------------------------------------------------
213
297
  export function registerGetCustomCodeTool(server) {
214
- server.tool("get_custom_code", "Get custom actions, functions, widgets, and AI agents from local cache — signatures, arguments, return types, and optionally Dart source code. No API calls. Run sync_project first if not cached.", {
298
+ server.tool("get_custom_code", "Get custom actions, functions, widgets, AI agents, app action components, and custom files from local cache — signatures, arguments, return types, and optionally Dart source code. No API calls. Run sync_project first if not cached.", {
215
299
  projectId: z.string().describe("The FlutterFlow project ID"),
216
300
  type: z
217
- .enum(["actions", "functions", "widgets", "agents", "all"])
301
+ .enum(["actions", "functions", "widgets", "agents", "app-actions", "custom-files", "all"])
218
302
  .optional()
219
303
  .default("all")
220
304
  .describe("Type of custom code to retrieve"),
@@ -240,7 +324,7 @@ export function registerGetCustomCodeTool(server) {
240
324
  };
241
325
  }
242
326
  const categories = type === "all"
243
- ? ["actions", "functions", "widgets", "agents"]
327
+ ? ["actions", "functions", "widgets", "agents", "app-actions", "custom-files"]
244
328
  : [type];
245
329
  const sections = [];
246
330
  for (const cat of categories) {
@@ -268,6 +352,18 @@ export function registerGetCustomCodeTool(server) {
268
352
  sections.push(`## AI Agents (${items.length})\n\n${items.map(formatAgent).join("\n\n")}`);
269
353
  }
270
354
  }
355
+ else if (cat === "app-actions") {
356
+ const items = await processAppActions(projectId, name);
357
+ if (items.length > 0) {
358
+ sections.push(`## App Action Components (${items.length})\n\n${items.map(formatAppAction).join("\n\n")}`);
359
+ }
360
+ }
361
+ else if (cat === "custom-files") {
362
+ const items = await processCustomFiles(projectId, name);
363
+ if (items.length > 0) {
364
+ sections.push(`## Custom Files (${items.length})\n\n${items.map(formatCustomFile).join("\n\n")}`);
365
+ }
366
+ }
271
367
  }
272
368
  if (sections.length === 0) {
273
369
  return {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * get_editing_guide tool — returns workflow steps, relevant YAML docs,
3
+ * and universal rules for a given FlutterFlow editing task.
4
+ * No API calls: reads from bundled docs/ff-yaml/ directory.
5
+ */
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ export declare function registerGetEditingGuideTool(server: McpServer): void;
@@ -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;