flutterflow-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +124 -0
  2. package/build/api/flutterflow.d.ts +11 -0
  3. package/build/api/flutterflow.js +61 -0
  4. package/build/index.d.ts +2 -0
  5. package/build/index.js +54 -0
  6. package/build/prompts/dev-workflow.d.ts +2 -0
  7. package/build/prompts/dev-workflow.js +68 -0
  8. package/build/prompts/generate-page.d.ts +2 -0
  9. package/build/prompts/generate-page.js +36 -0
  10. package/build/prompts/inspect-project.d.ts +2 -0
  11. package/build/prompts/inspect-project.js +30 -0
  12. package/build/prompts/modify-component.d.ts +2 -0
  13. package/build/prompts/modify-component.js +39 -0
  14. package/build/resources/docs.d.ts +2 -0
  15. package/build/resources/docs.js +76 -0
  16. package/build/resources/projects.d.ts +3 -0
  17. package/build/resources/projects.js +60 -0
  18. package/build/tools/find-component-usages.d.ts +7 -0
  19. package/build/tools/find-component-usages.js +225 -0
  20. package/build/tools/find-page-navigations.d.ts +7 -0
  21. package/build/tools/find-page-navigations.js +228 -0
  22. package/build/tools/get-component-summary.d.ts +22 -0
  23. package/build/tools/get-component-summary.js +193 -0
  24. package/build/tools/get-page-by-name.d.ts +3 -0
  25. package/build/tools/get-page-by-name.js +56 -0
  26. package/build/tools/get-page-summary.d.ts +22 -0
  27. package/build/tools/get-page-summary.js +220 -0
  28. package/build/tools/get-yaml-docs.d.ts +6 -0
  29. package/build/tools/get-yaml-docs.js +217 -0
  30. package/build/tools/get-yaml.d.ts +3 -0
  31. package/build/tools/get-yaml.js +47 -0
  32. package/build/tools/list-files.d.ts +3 -0
  33. package/build/tools/list-files.js +30 -0
  34. package/build/tools/list-pages.d.ts +25 -0
  35. package/build/tools/list-pages.js +101 -0
  36. package/build/tools/list-projects.d.ts +3 -0
  37. package/build/tools/list-projects.js +19 -0
  38. package/build/tools/sync-project.d.ts +3 -0
  39. package/build/tools/sync-project.js +144 -0
  40. package/build/tools/update-yaml.d.ts +3 -0
  41. package/build/tools/update-yaml.js +24 -0
  42. package/build/tools/validate-yaml.d.ts +3 -0
  43. package/build/tools/validate-yaml.js +22 -0
  44. package/build/utils/cache.d.ts +48 -0
  45. package/build/utils/cache.js +162 -0
  46. package/build/utils/decode-yaml.d.ts +7 -0
  47. package/build/utils/decode-yaml.js +31 -0
  48. package/build/utils/page-summary/action-summarizer.d.ts +9 -0
  49. package/build/utils/page-summary/action-summarizer.js +291 -0
  50. package/build/utils/page-summary/formatter.d.ts +13 -0
  51. package/build/utils/page-summary/formatter.js +121 -0
  52. package/build/utils/page-summary/node-extractor.d.ts +17 -0
  53. package/build/utils/page-summary/node-extractor.js +207 -0
  54. package/build/utils/page-summary/tree-walker.d.ts +6 -0
  55. package/build/utils/page-summary/tree-walker.js +55 -0
  56. package/build/utils/page-summary/types.d.ts +56 -0
  57. package/build/utils/page-summary/types.js +4 -0
  58. package/build/utils/parse-folders.d.ts +9 -0
  59. package/build/utils/parse-folders.js +29 -0
  60. package/docs/ff-yaml/00-overview.md +137 -0
  61. package/docs/ff-yaml/01-project-files.md +513 -0
  62. package/docs/ff-yaml/02-pages.md +572 -0
  63. package/docs/ff-yaml/03-components.md +413 -0
  64. package/docs/ff-yaml/04-widgets/README.md +122 -0
  65. package/docs/ff-yaml/04-widgets/button.md +444 -0
  66. package/docs/ff-yaml/04-widgets/container.md +358 -0
  67. package/docs/ff-yaml/04-widgets/dropdown.md +579 -0
  68. package/docs/ff-yaml/04-widgets/form.md +256 -0
  69. package/docs/ff-yaml/04-widgets/image.md +276 -0
  70. package/docs/ff-yaml/04-widgets/layout.md +355 -0
  71. package/docs/ff-yaml/04-widgets/misc.md +553 -0
  72. package/docs/ff-yaml/04-widgets/text-field.md +326 -0
  73. package/docs/ff-yaml/04-widgets/text.md +302 -0
  74. package/docs/ff-yaml/05-actions.md +843 -0
  75. package/docs/ff-yaml/06-variables.md +834 -0
  76. package/docs/ff-yaml/07-data.md +591 -0
  77. package/docs/ff-yaml/08-custom-code.md +715 -0
  78. package/docs/ff-yaml/09-theming.md +592 -0
  79. package/docs/ff-yaml/10-editing-guide.md +454 -0
  80. package/docs/ff-yaml/README.md +105 -0
  81. package/package.json +55 -0
  82. package/skills/ff-widget-patterns.md +141 -0
  83. package/skills/ff-yaml-dev.md +58 -0
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+ import { parseFolderMapping } from "../utils/parse-folders.js";
3
+ import { extractPageFileKeys, fetchOneFile } from "./list-pages.js";
4
+ export function registerGetPageByNameTool(server, client) {
5
+ server.tool("get_page_by_name", "Fetch a FlutterFlow page by its human-readable name (e.g. 'Welcome', 'GoldPass'). Resolves the name to the correct scaffold ID and returns the full page YAML. Case-insensitive matching.", {
6
+ projectId: z.string().describe("The FlutterFlow project ID"),
7
+ pageName: z
8
+ .string()
9
+ .describe("The human-readable page name (e.g. 'Welcome', 'GoldPass'). Case-insensitive."),
10
+ }, async ({ projectId, pageName }) => {
11
+ // Step 1: Get file list and folders (2 API calls)
12
+ const [fileNamesRaw, foldersResult] = await Promise.all([
13
+ client.listPartitionedFileNames(projectId),
14
+ fetchOneFile(client, projectId, "folders"),
15
+ ]);
16
+ const pageFileKeys = extractPageFileKeys(fileNamesRaw);
17
+ const folderMap = foldersResult
18
+ ? parseFolderMapping(foldersResult.content)
19
+ : {};
20
+ // Step 2: Search pages sequentially until we find the name match
21
+ const searchName = pageName.toLowerCase();
22
+ const available = [];
23
+ for (const fileKey of pageFileKeys) {
24
+ const result = await fetchOneFile(client, projectId, fileKey);
25
+ if (!result)
26
+ continue;
27
+ const nameMatch = result.content.match(/^name:\s*(.+)$/m);
28
+ const name = nameMatch ? nameMatch[1].trim() : "";
29
+ if (name.toLowerCase() === searchName) {
30
+ const scaffoldMatch = fileKey.match(/^page\/id-(Scaffold_\w+)$/);
31
+ const scaffoldId = scaffoldMatch
32
+ ? scaffoldMatch[1]
33
+ : fileKey;
34
+ const folder = folderMap[scaffoldId] || "(unmapped)";
35
+ return {
36
+ content: [
37
+ {
38
+ type: "text",
39
+ text: `# ${name} (${scaffoldId}) — folder: ${folder}\n# File key: ${fileKey}\n${result.content}`,
40
+ },
41
+ ],
42
+ };
43
+ }
44
+ if (name)
45
+ available.push(name);
46
+ }
47
+ return {
48
+ content: [
49
+ {
50
+ type: "text",
51
+ text: `Page "${pageName}" not found. Available pages:\n${available.map((n) => ` - ${n}`).join("\n")}`,
52
+ },
53
+ ],
54
+ };
55
+ });
56
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * get_page_summary tool — assembles cached sub-files into a readable page summary.
3
+ * Zero API calls: everything comes from the local .ff-cache.
4
+ */
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ interface ResolvedPage {
7
+ scaffoldId: string;
8
+ pageFileKey: string;
9
+ }
10
+ /**
11
+ * Resolve a page name or scaffold ID to its cache file key.
12
+ * Returns all available page names if no match found.
13
+ */
14
+ export declare function resolvePage(projectId: string, pageName?: string, scaffoldId?: string): Promise<{
15
+ ok: true;
16
+ page: ResolvedPage;
17
+ } | {
18
+ ok: false;
19
+ available: string[];
20
+ }>;
21
+ export declare function registerGetPageSummaryTool(server: McpServer): void;
22
+ export {};
@@ -0,0 +1,220 @@
1
+ import { z } from "zod";
2
+ import YAML from "yaml";
3
+ import { cacheRead, cacheMeta, listCachedKeys, } from "../utils/cache.js";
4
+ import { parseFolderMapping } from "../utils/parse-folders.js";
5
+ import { parseTreeOutline } from "../utils/page-summary/tree-walker.js";
6
+ import { extractNodeInfo } from "../utils/page-summary/node-extractor.js";
7
+ import { summarizeTriggers } from "../utils/page-summary/action-summarizer.js";
8
+ import { formatPageSummary } from "../utils/page-summary/formatter.js";
9
+ /**
10
+ * Resolve a page name or scaffold ID to its cache file key.
11
+ * Returns all available page names if no match found.
12
+ */
13
+ export async function resolvePage(projectId, pageName, scaffoldId) {
14
+ if (scaffoldId) {
15
+ const pageFileKey = `page/id-${scaffoldId}`;
16
+ const content = await cacheRead(projectId, pageFileKey);
17
+ if (content) {
18
+ return { ok: true, page: { scaffoldId, pageFileKey } };
19
+ }
20
+ }
21
+ // List all cached page top-level files
22
+ const allKeys = await listCachedKeys(projectId, "page/id-Scaffold_");
23
+ // Filter to top-level page files only (not sub-files)
24
+ const pageKeys = allKeys.filter((k) => /^page\/id-Scaffold_\w+$/.test(k));
25
+ const available = [];
26
+ for (const key of pageKeys) {
27
+ const content = await cacheRead(projectId, key);
28
+ if (!content)
29
+ continue;
30
+ const nameMatch = content.match(/^name:\s*(.+)$/m);
31
+ const name = nameMatch ? nameMatch[1].trim() : "";
32
+ if (pageName && name.toLowerCase() === pageName.toLowerCase()) {
33
+ const sid = key.match(/^page\/id-(Scaffold_\w+)$/)?.[1] || "";
34
+ return { ok: true, page: { scaffoldId: sid, pageFileKey: key } };
35
+ }
36
+ if (name)
37
+ available.push(name);
38
+ }
39
+ return { ok: false, available };
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // Metadata extraction
43
+ // ---------------------------------------------------------------------------
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
+ /** Extract page metadata (name, params, state) from the top-level page YAML. */
63
+ async function extractPageMeta(projectId, page, folder) {
64
+ const content = await cacheRead(projectId, page.pageFileKey);
65
+ if (!content) {
66
+ return {
67
+ pageName: page.scaffoldId,
68
+ scaffoldId: page.scaffoldId,
69
+ folder,
70
+ params: [],
71
+ stateFields: [],
72
+ };
73
+ }
74
+ const doc = YAML.parse(content);
75
+ const pageName = doc.name || page.scaffoldId;
76
+ // Params
77
+ const params = [];
78
+ const rawParams = doc.params;
79
+ if (rawParams) {
80
+ for (const val of Object.values(rawParams)) {
81
+ const id = val.identifier;
82
+ const name = id?.name || "unknown";
83
+ const dt = val.dataType || {};
84
+ const defaultVal = val.defaultValue;
85
+ params.push({
86
+ name,
87
+ dataType: resolveDataType(dt),
88
+ defaultValue: defaultVal?.serializedValue,
89
+ });
90
+ }
91
+ }
92
+ // State fields
93
+ const stateFields = [];
94
+ const classModel = doc.classModel;
95
+ const rawFields = classModel?.stateFields;
96
+ if (Array.isArray(rawFields)) {
97
+ for (const field of rawFields) {
98
+ const param = field.parameter;
99
+ if (!param)
100
+ continue;
101
+ const id = param.identifier;
102
+ const name = id?.name || "unknown";
103
+ const dt = param.dataType || {};
104
+ const defaultVals = field.serializedDefaultValue;
105
+ stateFields.push({
106
+ name,
107
+ dataType: resolveDataType(dt),
108
+ defaultValue: defaultVals?.[0],
109
+ });
110
+ }
111
+ }
112
+ return { pageName, scaffoldId: page.scaffoldId, folder, params, stateFields };
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // Tree enrichment
116
+ // ---------------------------------------------------------------------------
117
+ /**
118
+ * Recursively enrich an OutlineNode tree with node info and trigger summaries.
119
+ */
120
+ async function enrichNode(projectId, pagePrefix, // e.g. "page/id-Scaffold_xxx/page-widget-tree-outline"
121
+ outline) {
122
+ const nodeInfo = await extractNodeInfo(projectId, pagePrefix, outline.key);
123
+ // Check for triggers
124
+ const nodeFilePrefix = `${pagePrefix}/node/id-${outline.key}`;
125
+ const triggers = await summarizeTriggers(projectId, nodeFilePrefix);
126
+ // Enrich children
127
+ const children = await Promise.all(outline.children.map((child) => enrichNode(projectId, pagePrefix, child)));
128
+ return {
129
+ key: outline.key,
130
+ type: nodeInfo.type,
131
+ name: nodeInfo.name,
132
+ slot: outline.slot,
133
+ detail: nodeInfo.detail,
134
+ triggers,
135
+ children,
136
+ };
137
+ }
138
+ // ---------------------------------------------------------------------------
139
+ // Tool registration
140
+ // ---------------------------------------------------------------------------
141
+ export function registerGetPageSummaryTool(server) {
142
+ server.tool("get_page_summary", "Get a readable summary of a FlutterFlow page from local cache — widget tree, actions, params, state. No API calls. Run sync_project first if not cached.", {
143
+ projectId: z.string().describe("The FlutterFlow project ID"),
144
+ pageName: z
145
+ .string()
146
+ .optional()
147
+ .describe("Human-readable page name (e.g. 'PaywallPage'). Case-insensitive. Provide either pageName or scaffoldId."),
148
+ scaffoldId: z
149
+ .string()
150
+ .optional()
151
+ .describe("Scaffold ID (e.g. 'Scaffold_tydsj8ql'). Provide either pageName or scaffoldId."),
152
+ }, async ({ projectId, pageName, scaffoldId }) => {
153
+ // Check cache exists
154
+ const meta = await cacheMeta(projectId);
155
+ if (!meta) {
156
+ return {
157
+ content: [
158
+ {
159
+ type: "text",
160
+ text: `No cache found for project "${projectId}". Run sync_project first to download the project YAML files.`,
161
+ },
162
+ ],
163
+ };
164
+ }
165
+ if (!pageName && !scaffoldId) {
166
+ return {
167
+ content: [
168
+ {
169
+ type: "text",
170
+ text: "Provide either pageName or scaffoldId.",
171
+ },
172
+ ],
173
+ };
174
+ }
175
+ // Resolve page
176
+ const resolved = await resolvePage(projectId, pageName, scaffoldId);
177
+ if (!resolved.ok) {
178
+ const list = resolved.available.map((n) => ` - ${n}`).join("\n");
179
+ const searchTerm = pageName || scaffoldId || "";
180
+ return {
181
+ content: [
182
+ {
183
+ type: "text",
184
+ text: `Page "${searchTerm}" not found in cache. Available pages:\n${list}`,
185
+ },
186
+ ],
187
+ };
188
+ }
189
+ const page = resolved.page;
190
+ // Get folder mapping
191
+ const foldersContent = await cacheRead(projectId, "folders");
192
+ const folderMap = foldersContent
193
+ ? parseFolderMapping(foldersContent)
194
+ : {};
195
+ const folder = folderMap[page.scaffoldId] || "(unmapped)";
196
+ // Extract metadata
197
+ const pageMeta = await extractPageMeta(projectId, page, folder);
198
+ // Parse widget tree outline
199
+ const outlineKey = `${page.pageFileKey}/page-widget-tree-outline`;
200
+ const outlineContent = await cacheRead(projectId, outlineKey);
201
+ if (!outlineContent) {
202
+ return {
203
+ content: [
204
+ {
205
+ type: "text",
206
+ text: `Page "${pageMeta.pageName}" found but widget tree outline is not cached. Re-run sync_project to fetch all sub-files.`,
207
+ },
208
+ ],
209
+ };
210
+ }
211
+ const outlineTree = parseTreeOutline(outlineContent);
212
+ // Enrich the tree with node info and triggers
213
+ const enrichedTree = await enrichNode(projectId, outlineKey, outlineTree);
214
+ // Format output
215
+ const summary = formatPageSummary(pageMeta, enrichedTree);
216
+ return {
217
+ content: [{ type: "text", text: summary }],
218
+ };
219
+ });
220
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * get_yaml_docs tool — search and retrieve FlutterFlow YAML reference documentation.
3
+ * No API calls: reads from bundled docs/ff-yaml/ directory.
4
+ */
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ export declare function registerGetYamlDocsTool(server: McpServer): void;
@@ -0,0 +1,217 @@
1
+ import { z } from "zod";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const DOCS_DIR = path.resolve(__dirname, "../../docs/ff-yaml");
8
+ /** Topic-to-file mapping for fuzzy search. */
9
+ const TOPIC_MAP = {
10
+ // Widgets
11
+ button: "04-widgets/button.md",
12
+ iconbutton: "04-widgets/button.md",
13
+ text: "04-widgets/text.md",
14
+ richtext: "04-widgets/text.md",
15
+ richtextspan: "04-widgets/text.md",
16
+ textfield: "04-widgets/text-field.md",
17
+ "text-field": "04-widgets/text-field.md",
18
+ input: "04-widgets/text-field.md",
19
+ container: "04-widgets/container.md",
20
+ boxdecoration: "04-widgets/container.md",
21
+ column: "04-widgets/layout.md",
22
+ row: "04-widgets/layout.md",
23
+ stack: "04-widgets/layout.md",
24
+ wrap: "04-widgets/layout.md",
25
+ layout: "04-widgets/layout.md",
26
+ image: "04-widgets/image.md",
27
+ form: "04-widgets/form.md",
28
+ validation: "04-widgets/form.md",
29
+ dropdown: "04-widgets/dropdown.md",
30
+ choicechips: "04-widgets/dropdown.md",
31
+ icon: "04-widgets/misc.md",
32
+ progressbar: "04-widgets/misc.md",
33
+ appbar: "04-widgets/misc.md",
34
+ conditionalbuilder: "04-widgets/misc.md",
35
+ widget: "04-widgets/README.md",
36
+ widgets: "04-widgets/README.md",
37
+ // Non-widget topics
38
+ actions: "05-actions.md",
39
+ action: "05-actions.md",
40
+ trigger: "05-actions.md",
41
+ navigate: "05-actions.md",
42
+ navigation: "05-actions.md",
43
+ ontap: "05-actions.md",
44
+ variables: "06-variables.md",
45
+ variable: "06-variables.md",
46
+ binding: "06-variables.md",
47
+ "data-binding": "06-variables.md",
48
+ data: "07-data.md",
49
+ collections: "07-data.md",
50
+ firestore: "07-data.md",
51
+ api: "07-data.md",
52
+ custom: "08-custom-code.md",
53
+ dart: "08-custom-code.md",
54
+ "custom-code": "08-custom-code.md",
55
+ theme: "09-theming.md",
56
+ theming: "09-theming.md",
57
+ color: "09-theming.md",
58
+ colors: "09-theming.md",
59
+ font: "09-theming.md",
60
+ typography: "09-theming.md",
61
+ editing: "10-editing-guide.md",
62
+ workflow: "10-editing-guide.md",
63
+ "editing-guide": "10-editing-guide.md",
64
+ push: "10-editing-guide.md",
65
+ overview: "00-overview.md",
66
+ structure: "00-overview.md",
67
+ "project-files": "01-project-files.md",
68
+ config: "01-project-files.md",
69
+ settings: "01-project-files.md",
70
+ pages: "02-pages.md",
71
+ page: "02-pages.md",
72
+ scaffold: "02-pages.md",
73
+ components: "03-components.md",
74
+ component: "03-components.md",
75
+ // Universal patterns
76
+ inputvalue: "README.md",
77
+ mostrecentinputvalue: "README.md",
78
+ padding: "README.md",
79
+ "border-radius": "README.md",
80
+ };
81
+ /** List all doc files recursively. */
82
+ function listDocFiles(dir, prefix = "") {
83
+ const results = [];
84
+ if (!fs.existsSync(dir))
85
+ return results;
86
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
87
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
88
+ if (entry.isDirectory()) {
89
+ results.push(...listDocFiles(path.join(dir, entry.name), relPath));
90
+ }
91
+ else if (entry.name.endsWith(".md")) {
92
+ results.push(relPath);
93
+ }
94
+ }
95
+ return results;
96
+ }
97
+ /** Read a doc file. Returns null if not found. */
98
+ function readDoc(relPath) {
99
+ const filePath = path.join(DOCS_DIR, relPath);
100
+ if (!fs.existsSync(filePath))
101
+ return null;
102
+ return fs.readFileSync(filePath, "utf-8");
103
+ }
104
+ export function registerGetYamlDocsTool(server) {
105
+ server.tool("get_yaml_docs", "Search and retrieve FlutterFlow YAML reference documentation. Use `topic` to search by keyword (e.g. 'Button', 'actions', 'theming') or `file` to fetch a specific doc file. Returns the full doc content.", {
106
+ topic: z
107
+ .string()
108
+ .optional()
109
+ .describe("Search topic/keyword (e.g. 'Button', 'actions', 'theme', 'Column', 'variables'). Case-insensitive."),
110
+ file: z
111
+ .string()
112
+ .optional()
113
+ .describe("Specific doc file path (e.g. '04-widgets/button', '05-actions', 'README'). Omit .md extension."),
114
+ }, async ({ topic, file }) => {
115
+ // Direct file access
116
+ if (file) {
117
+ const content = readDoc(`${file}.md`);
118
+ if (content) {
119
+ return {
120
+ content: [{ type: "text", text: content }],
121
+ };
122
+ }
123
+ const available = listDocFiles(DOCS_DIR)
124
+ .map((f) => ` - ${f.replace(/\.md$/, "")}`)
125
+ .join("\n");
126
+ return {
127
+ content: [
128
+ {
129
+ type: "text",
130
+ text: `Doc file not found: "${file}"\n\nAvailable files:\n${available}`,
131
+ },
132
+ ],
133
+ };
134
+ }
135
+ // Topic search
136
+ if (topic) {
137
+ const key = topic.toLowerCase().replace(/[\s_-]+/g, "");
138
+ const matchedFile = TOPIC_MAP[key];
139
+ if (matchedFile) {
140
+ const content = readDoc(matchedFile);
141
+ if (content) {
142
+ return {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: `# Matched: ${matchedFile}\n\n${content}`,
147
+ },
148
+ ],
149
+ };
150
+ }
151
+ }
152
+ // Fallback: scan filenames and file contents for the topic
153
+ const allFiles = listDocFiles(DOCS_DIR);
154
+ const matches = [];
155
+ for (const f of allFiles) {
156
+ // Check filename match
157
+ if (f.toLowerCase().includes(topic.toLowerCase())) {
158
+ matches.push(f);
159
+ continue;
160
+ }
161
+ // Check content match (first-level scan)
162
+ const content = readDoc(f);
163
+ if (content &&
164
+ content.toLowerCase().includes(topic.toLowerCase())) {
165
+ matches.push(f);
166
+ }
167
+ }
168
+ if (matches.length === 0) {
169
+ const available = allFiles
170
+ .map((f) => ` - ${f.replace(/\.md$/, "")}`)
171
+ .join("\n");
172
+ return {
173
+ content: [
174
+ {
175
+ type: "text",
176
+ text: `No docs found for topic "${topic}".\n\nAvailable docs:\n${available}`,
177
+ },
178
+ ],
179
+ };
180
+ }
181
+ // Return the first match content, list others
182
+ const primary = readDoc(matches[0]) || "";
183
+ const others = matches.length > 1
184
+ ? `\n\n---\n\nOther matching docs:\n${matches
185
+ .slice(1)
186
+ .map((f) => ` - ${f.replace(/\.md$/, "")}`)
187
+ .join("\n")}`
188
+ : "";
189
+ return {
190
+ content: [
191
+ {
192
+ type: "text",
193
+ text: `# Matched: ${matches[0]}\n\n${primary}${others}`,
194
+ },
195
+ ],
196
+ };
197
+ }
198
+ // No params: return index
199
+ const readme = readDoc("README.md");
200
+ if (readme) {
201
+ return {
202
+ content: [{ type: "text", text: readme }],
203
+ };
204
+ }
205
+ const available = listDocFiles(DOCS_DIR)
206
+ .map((f) => ` - ${f.replace(/\.md$/, "")}`)
207
+ .join("\n");
208
+ return {
209
+ content: [
210
+ {
211
+ type: "text",
212
+ text: `FlutterFlow YAML Reference Docs:\n${available}\n\nUse the 'topic' or 'file' parameter to fetch specific docs.`,
213
+ },
214
+ ],
215
+ };
216
+ });
217
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { FlutterFlowClient } from "../api/flutterflow.js";
3
+ export declare function registerGetYamlTool(server: McpServer, client: FlutterFlowClient): void;
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+ import { decodeProjectYamlResponse } from "../utils/decode-yaml.js";
3
+ import { cacheRead, cacheWrite } from "../utils/cache.js";
4
+ export function registerGetYamlTool(server, client) {
5
+ server.tool("get_project_yaml", "Download YAML files from a FlutterFlow project. Returns one file if fileName is specified, otherwise returns all files.", {
6
+ projectId: z.string().describe("The FlutterFlow project ID"),
7
+ fileName: z
8
+ .string()
9
+ .optional()
10
+ .describe("Specific YAML file name to download (e.g. 'app-details', 'page/id-xxx'). Omit to get all files."),
11
+ }, async ({ projectId, fileName }) => {
12
+ // Cache-first for single-file requests
13
+ if (fileName) {
14
+ const cached = await cacheRead(projectId, fileName);
15
+ if (cached) {
16
+ return {
17
+ content: [
18
+ {
19
+ type: "text",
20
+ text: `# ${fileName} (cached)\n${cached}`,
21
+ },
22
+ ],
23
+ };
24
+ }
25
+ }
26
+ const raw = await client.getProjectYamls(projectId, fileName);
27
+ const decoded = decodeProjectYamlResponse(raw);
28
+ // Write fetched results to cache (strip .yaml from ZIP entry names to avoid double extension)
29
+ for (const [name, yaml] of Object.entries(decoded)) {
30
+ const cleanName = name.endsWith(".yaml") ? name.slice(0, -".yaml".length) : name;
31
+ await cacheWrite(projectId, cleanName, yaml);
32
+ }
33
+ const entries = Object.entries(decoded);
34
+ if (entries.length === 1) {
35
+ const [name, yaml] = entries[0];
36
+ return {
37
+ content: [{ type: "text", text: `# ${name}\n${yaml}` }],
38
+ };
39
+ }
40
+ return {
41
+ content: entries.map(([name, yaml]) => ({
42
+ type: "text",
43
+ text: `# ${name}\n${yaml}`,
44
+ })),
45
+ };
46
+ });
47
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { FlutterFlowClient } from "../api/flutterflow.js";
3
+ export declare function registerListFilesTool(server: McpServer, client: FlutterFlowClient): void;
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+ import { listCachedKeys, cacheMeta } from "../utils/cache.js";
3
+ export function registerListFilesTool(server, client) {
4
+ server.tool("list_project_files", "List all YAML file names in a FlutterFlow project", {
5
+ projectId: z.string().describe("The FlutterFlow project ID"),
6
+ }, async ({ projectId }) => {
7
+ // If cache exists, return file keys from cache
8
+ const meta = await cacheMeta(projectId);
9
+ if (meta) {
10
+ const keys = await listCachedKeys(projectId);
11
+ return {
12
+ content: [
13
+ {
14
+ type: "text",
15
+ text: JSON.stringify({ value: { file_names: keys }, source: "cache", syncedAt: meta.lastSyncedAt }, null, 2),
16
+ },
17
+ ],
18
+ };
19
+ }
20
+ const result = await client.listPartitionedFileNames(projectId);
21
+ return {
22
+ content: [
23
+ {
24
+ type: "text",
25
+ text: JSON.stringify(result, null, 2),
26
+ },
27
+ ],
28
+ };
29
+ });
30
+ }
@@ -0,0 +1,25 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { FlutterFlowClient } from "../api/flutterflow.js";
3
+ export interface PageInfo {
4
+ scaffoldId: string;
5
+ name: string;
6
+ folder: string;
7
+ fileKey: string;
8
+ }
9
+ /**
10
+ * Extract page scaffold IDs from the partitioned file names list.
11
+ * Filters for top-level page files only (not sub-files like widget-tree-outline).
12
+ */
13
+ export declare function extractPageFileKeys(fileNames: unknown): string[];
14
+ /**
15
+ * Fetch a single YAML file and decode it. Returns null on failure.
16
+ */
17
+ export declare function fetchOneFile(client: FlutterFlowClient, projectId: string, fileName: string): Promise<{
18
+ fileKey: string;
19
+ content: string;
20
+ } | null>;
21
+ /**
22
+ * Extract a compact page index by fetching pages in batches.
23
+ */
24
+ export declare function listPages(client: FlutterFlowClient, projectId: string): Promise<PageInfo[]>;
25
+ export declare function registerListPagesTool(server: McpServer, client: FlutterFlowClient): void;