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 +14 -2
- package/build/prompts/generate-page.js +6 -5
- package/build/prompts/modify-component.js +6 -5
- package/build/tools/get-app-settings.d.ts +2 -0
- package/build/tools/get-app-settings.js +169 -0
- package/build/tools/get-custom-code.js +99 -3
- package/build/tools/get-editing-guide.d.ts +7 -0
- package/build/tools/get-editing-guide.js +185 -0
- package/build/tools/get-general-settings.d.ts +2 -0
- package/build/tools/get-general-settings.js +116 -0
- package/build/tools/get-in-app-purchases.d.ts +2 -0
- package/build/tools/get-in-app-purchases.js +51 -0
- package/build/tools/get-integrations.d.ts +2 -0
- package/build/tools/get-integrations.js +137 -0
- package/build/tools/get-project-setup.d.ts +2 -0
- package/build/tools/get-project-setup.js +212 -0
- package/build/tools/get-yaml-docs.js +1 -111
- package/build/tools/list-files.js +22 -3
- package/build/tools/search-project-files.d.ts +2 -0
- package/build/tools/search-project-files.js +69 -0
- package/build/tools/update-yaml.js +1 -1
- package/build/tools/validate-yaml.js +19 -2
- package/build/utils/topic-map.d.ts +7 -0
- package/build/utils/topic-map.js +115 -0
- package/package.json +1 -1
- package/build/tools/get-project-config.d.ts +0 -2
- package/build/tools/get-project-config.js +0 -160
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.
|
|
20
|
-
3.
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
23
|
-
3.
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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,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,
|
|
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
|
+
}
|