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,380 @@
1
+ import { z } from "zod";
2
+ import YAML from "yaml";
3
+ import { cacheRead, cacheMeta, listCachedKeys } from "../utils/cache.js";
4
+ import { batchProcess } from "../utils/batch-process.js";
5
+ import { resolveDataType } from "../utils/resolve-data-type.js";
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+ function formatArg(arg) {
10
+ return arg.required ? `${arg.name}: ${arg.type} (required)` : `${arg.name}: ${arg.type}?`;
11
+ }
12
+ function formatAction(action) {
13
+ const lines = [];
14
+ lines.push(`### ${action.name}`);
15
+ lines.push(`Key: ${action.key} | File: \`${action.fileKey}\``);
16
+ if (action.args.length === 0) {
17
+ lines.push("Args: (none)");
18
+ }
19
+ else {
20
+ lines.push(`Args: ${action.args.map(formatArg).join(", ")}`);
21
+ }
22
+ lines.push(`Returns: ${action.returnType ?? "void"}`);
23
+ lines.push(`Context: ${action.includeContext ? "Yes" : "No"}`);
24
+ if (action.code !== undefined) {
25
+ lines.push("");
26
+ lines.push("```dart");
27
+ lines.push(action.code);
28
+ lines.push("```");
29
+ }
30
+ return lines.join("\n");
31
+ }
32
+ function formatFunction(fn) {
33
+ const lines = [];
34
+ lines.push(`### ${fn.name}`);
35
+ lines.push(`Key: ${fn.key} | File: \`${fn.fileKey}\``);
36
+ if (fn.args.length === 0) {
37
+ lines.push("Args: (none)");
38
+ }
39
+ else {
40
+ lines.push(`Args: ${fn.args.map(formatArg).join(", ")}`);
41
+ }
42
+ lines.push(`Returns: ${fn.returnType ?? "void"}`);
43
+ if (fn.code !== undefined) {
44
+ lines.push("");
45
+ lines.push("```dart");
46
+ lines.push(fn.code);
47
+ lines.push("```");
48
+ }
49
+ return lines.join("\n");
50
+ }
51
+ function formatWidget(widget) {
52
+ const lines = [];
53
+ lines.push(`### ${widget.name}`);
54
+ lines.push(`Key: ${widget.key} | File: \`${widget.fileKey}\``);
55
+ if (widget.params.length === 0) {
56
+ lines.push("Params: (none)");
57
+ }
58
+ else {
59
+ lines.push("Params:");
60
+ for (const p of widget.params) {
61
+ const suffix = p.required ? " (required)" : "";
62
+ lines.push(` - ${p.name}: ${p.type}${suffix}`);
63
+ }
64
+ }
65
+ if (widget.code !== undefined) {
66
+ lines.push("");
67
+ lines.push("```dart");
68
+ lines.push(widget.code);
69
+ lines.push("```");
70
+ }
71
+ return lines.join("\n");
72
+ }
73
+ function formatAgent(agent) {
74
+ const lines = [];
75
+ lines.push(`### ${agent.displayName} [${agent.status}]`);
76
+ lines.push(`Key: ${agent.key} | File: \`${agent.fileKey}\``);
77
+ lines.push(`Provider: ${agent.provider} (${agent.model})`);
78
+ lines.push(`Input: ${agent.requestTypes.join(", ")}`);
79
+ lines.push(`Output: ${agent.responseType}`);
80
+ if (agent.description) {
81
+ lines.push(`Description: ${agent.description}`);
82
+ }
83
+ return lines.join("\n");
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
+ }
103
+ function resolveReturnType(returnParam) {
104
+ if (!returnParam)
105
+ return null;
106
+ const dt = returnParam.dataType;
107
+ if (!dt)
108
+ return null;
109
+ const resolved = resolveDataType(dt);
110
+ const nonNullable = dt.nonNullable ?? false;
111
+ return nonNullable ? `${resolved} (required)` : `${resolved}?`;
112
+ }
113
+ function parseArgs(rawArgs) {
114
+ if (!Array.isArray(rawArgs))
115
+ return [];
116
+ return rawArgs.map((arg) => {
117
+ const id = arg.identifier;
118
+ const name = id?.name || "unknown";
119
+ const dt = arg.dataType || {};
120
+ const type = resolveDataType(dt);
121
+ const required = dt.nonNullable ?? false;
122
+ return { name, type, required };
123
+ });
124
+ }
125
+ // ---------------------------------------------------------------------------
126
+ // Category processors
127
+ // ---------------------------------------------------------------------------
128
+ async function processActions(projectId, nameFilter, includeCode) {
129
+ const allKeys = await listCachedKeys(projectId, "custom-actions/id-");
130
+ const topKeys = allKeys.filter((k) => /^custom-actions\/id-[a-z0-9]+$/i.test(k));
131
+ return batchProcess(topKeys, 10, async (fileKey) => {
132
+ const content = await cacheRead(projectId, fileKey);
133
+ if (!content)
134
+ return null;
135
+ const doc = YAML.parse(content);
136
+ const id = doc.identifier;
137
+ const name = id?.name || "unknown";
138
+ if (nameFilter && name.toLowerCase() !== nameFilter.toLowerCase())
139
+ return null;
140
+ const idKey = id?.key || fileKey.match(/id-([a-z0-9]+)$/i)?.[1] || "unknown";
141
+ const args = parseArgs(doc.arguments);
142
+ const returnType = resolveReturnType(doc.returnParameter);
143
+ const includeContext = doc.includeContext ?? false;
144
+ let code;
145
+ if (includeCode) {
146
+ const codeContent = await cacheRead(projectId, `custom-actions/id-${idKey}/action-code.dart`);
147
+ if (codeContent)
148
+ code = codeContent;
149
+ }
150
+ return { name, key: idKey, fileKey, args, returnType, includeContext, code };
151
+ }).then((results) => results.filter((r) => r !== null));
152
+ }
153
+ async function processFunctions(projectId, nameFilter, includeCode) {
154
+ const allKeys = await listCachedKeys(projectId, "custom-functions/id-");
155
+ const topKeys = allKeys.filter((k) => /^custom-functions\/id-[a-z0-9]+$/i.test(k));
156
+ return batchProcess(topKeys, 10, async (fileKey) => {
157
+ const content = await cacheRead(projectId, fileKey);
158
+ if (!content)
159
+ return null;
160
+ const doc = YAML.parse(content);
161
+ const id = doc.identifier;
162
+ const name = id?.name || "unknown";
163
+ if (nameFilter && name.toLowerCase() !== nameFilter.toLowerCase())
164
+ return null;
165
+ const idKey = id?.key || fileKey.match(/id-([a-z0-9]+)$/i)?.[1] || "unknown";
166
+ const args = parseArgs(doc.arguments);
167
+ const returnType = resolveReturnType(doc.returnParameter);
168
+ let code;
169
+ if (includeCode) {
170
+ const codeContent = await cacheRead(projectId, `custom-functions/id-${idKey}/function-code.dart`);
171
+ if (codeContent)
172
+ code = codeContent;
173
+ }
174
+ return { name, key: idKey, fileKey, args, returnType, code };
175
+ }).then((results) => results.filter((r) => r !== null));
176
+ }
177
+ async function processWidgets(projectId, nameFilter, includeCode) {
178
+ const allKeys = await listCachedKeys(projectId, "custom-widgets/id-");
179
+ const topKeys = allKeys.filter((k) => /^custom-widgets\/id-[a-z0-9]+$/i.test(k));
180
+ return batchProcess(topKeys, 10, async (fileKey) => {
181
+ const content = await cacheRead(projectId, fileKey);
182
+ if (!content)
183
+ return null;
184
+ const doc = YAML.parse(content);
185
+ const id = doc.identifier;
186
+ const name = id?.name || "unknown";
187
+ if (nameFilter && name.toLowerCase() !== nameFilter.toLowerCase())
188
+ return null;
189
+ const idKey = id?.key || fileKey.match(/id-([a-z0-9]+)$/i)?.[1] || "unknown";
190
+ const rawParams = doc.parameters;
191
+ const params = parseArgs(rawParams);
192
+ const description = doc.description || "";
193
+ let code;
194
+ if (includeCode) {
195
+ const codeContent = await cacheRead(projectId, `custom-widgets/id-${idKey}/widget-code.dart`);
196
+ if (codeContent)
197
+ code = codeContent;
198
+ }
199
+ return { name, key: idKey, fileKey, params, description, code };
200
+ }).then((results) => results.filter((r) => r !== null));
201
+ }
202
+ async function processAgents(projectId, nameFilter) {
203
+ const allKeys = await listCachedKeys(projectId, "agent/id-");
204
+ const topKeys = allKeys.filter((k) => /^agent\/id-[a-z0-9]+$/i.test(k));
205
+ return batchProcess(topKeys, 10, async (fileKey) => {
206
+ const content = await cacheRead(projectId, fileKey);
207
+ if (!content)
208
+ return null;
209
+ const doc = YAML.parse(content);
210
+ const id = doc.identifier;
211
+ const identifierName = id?.name || "unknown";
212
+ if (nameFilter && identifierName.toLowerCase() !== nameFilter.toLowerCase())
213
+ return null;
214
+ const idKey = id?.key || fileKey.match(/id-([a-z0-9]+)$/i)?.[1] || "unknown";
215
+ const displayName = doc.name || identifierName;
216
+ const status = doc.status || "UNKNOWN";
217
+ const aiModel = doc.aiModel;
218
+ const provider = aiModel?.provider || "UNKNOWN";
219
+ const model = aiModel?.model || "unknown";
220
+ const reqOpts = doc.requestOptions;
221
+ const requestTypes = reqOpts?.requestTypes || [];
222
+ const resOpts = doc.responseOptions;
223
+ const responseType = resOpts?.responseType || "UNKNOWN";
224
+ const description = doc.description || "";
225
+ return { name: identifierName, key: idKey, fileKey, displayName, status, provider, model, requestTypes, responseType, description };
226
+ }).then((results) => results.filter((r) => r !== null));
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
+ }
294
+ // ---------------------------------------------------------------------------
295
+ // Tool registration
296
+ // ---------------------------------------------------------------------------
297
+ export function registerGetCustomCodeTool(server) {
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.", {
299
+ projectId: z.string().describe("The FlutterFlow project ID"),
300
+ type: z
301
+ .enum(["actions", "functions", "widgets", "agents", "app-actions", "custom-files", "all"])
302
+ .optional()
303
+ .default("all")
304
+ .describe("Type of custom code to retrieve"),
305
+ name: z
306
+ .string()
307
+ .optional()
308
+ .describe("Case-insensitive filter on identifier name"),
309
+ includeCode: z
310
+ .boolean()
311
+ .optional()
312
+ .default(false)
313
+ .describe("Include Dart source code in output"),
314
+ }, async ({ projectId, type, name, includeCode }) => {
315
+ const meta = await cacheMeta(projectId);
316
+ if (!meta) {
317
+ return {
318
+ content: [
319
+ {
320
+ type: "text",
321
+ text: `No cache found for project "${projectId}". Run sync_project first to download the project YAML files.`,
322
+ },
323
+ ],
324
+ };
325
+ }
326
+ const categories = type === "all"
327
+ ? ["actions", "functions", "widgets", "agents", "app-actions", "custom-files"]
328
+ : [type];
329
+ const sections = [];
330
+ for (const cat of categories) {
331
+ if (cat === "actions") {
332
+ const items = await processActions(projectId, name, includeCode);
333
+ if (items.length > 0) {
334
+ sections.push(`## Custom Actions (${items.length})\n\n${items.map(formatAction).join("\n\n")}`);
335
+ }
336
+ }
337
+ else if (cat === "functions") {
338
+ const items = await processFunctions(projectId, name, includeCode);
339
+ if (items.length > 0) {
340
+ sections.push(`## Custom Functions (${items.length})\n\n${items.map(formatFunction).join("\n\n")}`);
341
+ }
342
+ }
343
+ else if (cat === "widgets") {
344
+ const items = await processWidgets(projectId, name, includeCode);
345
+ if (items.length > 0) {
346
+ sections.push(`## Custom Widgets (${items.length})\n\n${items.map(formatWidget).join("\n\n")}`);
347
+ }
348
+ }
349
+ else if (cat === "agents") {
350
+ const items = await processAgents(projectId, name);
351
+ if (items.length > 0) {
352
+ sections.push(`## AI Agents (${items.length})\n\n${items.map(formatAgent).join("\n\n")}`);
353
+ }
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
+ }
367
+ }
368
+ if (sections.length === 0) {
369
+ return {
370
+ content: [
371
+ { type: "text", text: "No custom code found in cache." },
372
+ ],
373
+ };
374
+ }
375
+ const output = `# Custom Code\n\n${sections.join("\n\n")}`;
376
+ return {
377
+ content: [{ type: "text", text: output }],
378
+ };
379
+ });
380
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerGetDataModelsTool(server: McpServer): void;
@@ -0,0 +1,266 @@
1
+ import { z } from "zod";
2
+ import YAML from "yaml";
3
+ import { cacheRead, cacheMeta, listCachedKeys } from "../utils/cache.js";
4
+ import { batchProcess } from "../utils/batch-process.js";
5
+ import { resolveDataType } from "../utils/resolve-data-type.js";
6
+ async function readStructs(projectId, nameFilter) {
7
+ const allKeys = await listCachedKeys(projectId, "data-structs/id-");
8
+ const topLevel = allKeys.filter((k) => /^data-structs\/id-[a-z0-9]+$/i.test(k));
9
+ const results = await batchProcess(topLevel, 10, async (key) => {
10
+ const content = await cacheRead(projectId, key);
11
+ if (!content)
12
+ return null;
13
+ let doc;
14
+ try {
15
+ doc = YAML.parse(content);
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ const identifier = doc.identifier;
21
+ const name = identifier?.name || "unknown";
22
+ if (nameFilter && name.toLowerCase() !== nameFilter.toLowerCase())
23
+ return null;
24
+ const rawFields = doc.fields;
25
+ const fields = [];
26
+ if (Array.isArray(rawFields)) {
27
+ for (const f of rawFields) {
28
+ const fId = f.identifier;
29
+ const fName = fId?.name || "unknown";
30
+ const dt = f.dataType || {};
31
+ fields.push({ name: fName, type: resolveDataType(dt) });
32
+ }
33
+ }
34
+ return { name, fields };
35
+ });
36
+ return results.filter((r) => r !== null);
37
+ }
38
+ async function readEnums(projectId, nameFilter) {
39
+ const allKeys = await listCachedKeys(projectId, "enums/id-");
40
+ const topLevel = allKeys.filter((k) => /^enums\/id-[a-z0-9]+$/i.test(k));
41
+ const results = await batchProcess(topLevel, 10, async (key) => {
42
+ const content = await cacheRead(projectId, key);
43
+ if (!content)
44
+ return null;
45
+ let doc;
46
+ try {
47
+ doc = YAML.parse(content);
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ const identifier = doc.identifier;
53
+ const name = identifier?.name || "unknown";
54
+ if (nameFilter && name.toLowerCase() !== nameFilter.toLowerCase())
55
+ return null;
56
+ const elements = doc.elements;
57
+ const values = [];
58
+ if (Array.isArray(elements)) {
59
+ for (const el of elements) {
60
+ const elId = el.identifier;
61
+ const elName = elId?.name || "unknown";
62
+ values.push(elName);
63
+ }
64
+ }
65
+ return { name, values };
66
+ });
67
+ return results.filter((r) => r !== null);
68
+ }
69
+ async function readCollections(projectId, nameFilter) {
70
+ const allKeys = await listCachedKeys(projectId, "collections/id-");
71
+ const topLevel = allKeys.filter((k) => /^collections\/id-[a-z0-9]+$/i.test(k));
72
+ const results = await batchProcess(topLevel, 10, async (key) => {
73
+ const content = await cacheRead(projectId, key);
74
+ if (!content)
75
+ return null;
76
+ let doc;
77
+ try {
78
+ doc = YAML.parse(content);
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ const identifier = doc.identifier;
84
+ const name = identifier?.name || "unknown";
85
+ if (nameFilter && name.toLowerCase() !== nameFilter.toLowerCase())
86
+ return null;
87
+ const parentId = doc.parentCollectionIdentifier;
88
+ const parentName = parentId ? (parentId.name || null) : null;
89
+ const rawFields = doc.fields;
90
+ const fields = [];
91
+ if (rawFields && typeof rawFields === "object") {
92
+ for (const fieldVal of Object.values(rawFields)) {
93
+ const fId = fieldVal.identifier;
94
+ const fName = fId?.name || "unknown";
95
+ const dt = fieldVal.dataType || {};
96
+ let typeStr = resolveDataType(dt);
97
+ if (dt.scalarType === "DocumentReference") {
98
+ const sub = dt.subType;
99
+ const colId = sub?.collectionIdentifier;
100
+ if (colId?.name) {
101
+ typeStr = `DocumentReference \u2192 ${colId.name}`;
102
+ }
103
+ }
104
+ fields.push({ name: fName, type: typeStr });
105
+ }
106
+ }
107
+ return { name, parentName, fields };
108
+ });
109
+ return results.filter((r) => r !== null);
110
+ }
111
+ async function readSupabaseTables(projectId, nameFilter) {
112
+ const content = await cacheRead(projectId, "supabase");
113
+ if (!content)
114
+ return [];
115
+ let doc;
116
+ try {
117
+ doc = YAML.parse(content);
118
+ }
119
+ catch {
120
+ return [];
121
+ }
122
+ const dbConfig = doc.databaseConfig;
123
+ const tables = dbConfig?.tables;
124
+ if (!Array.isArray(tables))
125
+ return [];
126
+ const results = [];
127
+ for (const table of tables) {
128
+ const identifier = table.identifier;
129
+ const name = identifier?.name || "unknown";
130
+ if (nameFilter && name.toLowerCase() !== nameFilter.toLowerCase())
131
+ continue;
132
+ const rawFields = table.fields;
133
+ const fields = [];
134
+ if (Array.isArray(rawFields)) {
135
+ for (const f of rawFields) {
136
+ const fId = f.identifier;
137
+ const fName = fId?.name || "unknown";
138
+ const typeObj = f.type;
139
+ const dt = typeObj?.dataType || {};
140
+ fields.push({
141
+ name: fName,
142
+ type: resolveDataType(dt),
143
+ postgresType: f.postgresType || null,
144
+ isPrimaryKey: f.isPrimaryKey || false,
145
+ isRequired: f.isRequired || false,
146
+ hasDefault: f.hasDefault || false,
147
+ foreignKey: f.foreignKey || null,
148
+ });
149
+ }
150
+ }
151
+ results.push({ name, fields });
152
+ }
153
+ return results;
154
+ }
155
+ function formatOutput(structs, enums, collections, supabaseTables, nameFilter) {
156
+ const total = structs.length + enums.length + collections.length + supabaseTables.length;
157
+ if (total === 0) {
158
+ if (nameFilter)
159
+ return `No data models matching '${nameFilter}' found.`;
160
+ return "No data models found in cache.";
161
+ }
162
+ const sections = [];
163
+ sections.push("# Data Models");
164
+ if (structs.length > 0) {
165
+ sections.push("");
166
+ sections.push(`## Data Structs (${structs.length})`);
167
+ for (const s of structs) {
168
+ sections.push("");
169
+ sections.push(`### ${s.name}`);
170
+ for (const f of s.fields) {
171
+ sections.push(`- ${f.name}: ${f.type}`);
172
+ }
173
+ }
174
+ }
175
+ if (enums.length > 0) {
176
+ sections.push("");
177
+ sections.push(`## Enums (${enums.length})`);
178
+ for (const e of enums) {
179
+ sections.push("");
180
+ sections.push(`### ${e.name}`);
181
+ sections.push(`Values: ${e.values.join(", ")}`);
182
+ }
183
+ }
184
+ if (collections.length > 0) {
185
+ sections.push("");
186
+ sections.push(`## Collections (${collections.length})`);
187
+ for (const c of collections) {
188
+ sections.push("");
189
+ const suffix = c.parentName ? ` (sub-collection of ${c.parentName})` : "";
190
+ sections.push(`### ${c.name}${suffix}`);
191
+ for (const f of c.fields) {
192
+ sections.push(`- ${f.name}: ${f.type}`);
193
+ }
194
+ }
195
+ }
196
+ if (supabaseTables.length > 0) {
197
+ sections.push("");
198
+ sections.push(`## Supabase Tables (${supabaseTables.length})`);
199
+ for (const t of supabaseTables) {
200
+ sections.push("");
201
+ sections.push(`### ${t.name}`);
202
+ for (const f of t.fields) {
203
+ const flags = [];
204
+ if (f.isPrimaryKey)
205
+ flags.push("PK");
206
+ if (f.isRequired)
207
+ flags.push("required");
208
+ if (f.hasDefault)
209
+ flags.push("has default");
210
+ if (f.foreignKey)
211
+ flags.push(`FK \u2192 ${f.foreignKey}`);
212
+ const pgType = f.postgresType ? ` [${f.postgresType}]` : "";
213
+ const flagStr = flags.length > 0 ? ` (${flags.join(", ")})` : "";
214
+ sections.push(`- ${f.name}: ${f.type}${pgType}${flagStr}`);
215
+ }
216
+ }
217
+ }
218
+ return sections.join("\n");
219
+ }
220
+ export function registerGetDataModelsTool(server) {
221
+ server.tool("get_data_models", "Get data structs, enums, Firestore collections, and Supabase tables from local cache. No API calls. Run sync_project first if not cached.", {
222
+ projectId: z.string().describe("The FlutterFlow project ID"),
223
+ type: z
224
+ .enum(["structs", "enums", "collections", "supabase", "all"])
225
+ .optional()
226
+ .default("all")
227
+ .describe("Filter by model type: structs, enums, collections, supabase, or all (default)."),
228
+ name: z
229
+ .string()
230
+ .optional()
231
+ .describe("Case-insensitive filter by identifier name."),
232
+ }, async ({ projectId, type, name }) => {
233
+ const meta = await cacheMeta(projectId);
234
+ if (!meta) {
235
+ return {
236
+ content: [
237
+ {
238
+ type: "text",
239
+ text: `No cache found for project "${projectId}". Run sync_project first to download the project YAML files.`,
240
+ },
241
+ ],
242
+ };
243
+ }
244
+ const modelType = (type || "all");
245
+ let structs = [];
246
+ let enums = [];
247
+ let collections = [];
248
+ let supabaseTables = [];
249
+ if (modelType === "all" || modelType === "structs") {
250
+ structs = await readStructs(projectId, name);
251
+ }
252
+ if (modelType === "all" || modelType === "enums") {
253
+ enums = await readEnums(projectId, name);
254
+ }
255
+ if (modelType === "all" || modelType === "collections") {
256
+ collections = await readCollections(projectId, name);
257
+ }
258
+ if (modelType === "all" || modelType === "supabase") {
259
+ supabaseTables = await readSupabaseTables(projectId, name);
260
+ }
261
+ const output = formatOutput(structs, enums, collections, supabaseTables, name);
262
+ return {
263
+ content: [{ type: "text", text: output }],
264
+ };
265
+ });
266
+ }
@@ -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;