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.
- package/README.md +124 -0
- package/build/api/flutterflow.d.ts +11 -0
- package/build/api/flutterflow.js +61 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +54 -0
- package/build/prompts/dev-workflow.d.ts +2 -0
- package/build/prompts/dev-workflow.js +68 -0
- package/build/prompts/generate-page.d.ts +2 -0
- package/build/prompts/generate-page.js +36 -0
- package/build/prompts/inspect-project.d.ts +2 -0
- package/build/prompts/inspect-project.js +30 -0
- package/build/prompts/modify-component.d.ts +2 -0
- package/build/prompts/modify-component.js +39 -0
- package/build/resources/docs.d.ts +2 -0
- package/build/resources/docs.js +76 -0
- package/build/resources/projects.d.ts +3 -0
- package/build/resources/projects.js +60 -0
- package/build/tools/find-component-usages.d.ts +7 -0
- package/build/tools/find-component-usages.js +225 -0
- package/build/tools/find-page-navigations.d.ts +7 -0
- package/build/tools/find-page-navigations.js +228 -0
- package/build/tools/get-component-summary.d.ts +22 -0
- package/build/tools/get-component-summary.js +193 -0
- package/build/tools/get-page-by-name.d.ts +3 -0
- package/build/tools/get-page-by-name.js +56 -0
- package/build/tools/get-page-summary.d.ts +22 -0
- package/build/tools/get-page-summary.js +220 -0
- package/build/tools/get-yaml-docs.d.ts +6 -0
- package/build/tools/get-yaml-docs.js +217 -0
- package/build/tools/get-yaml.d.ts +3 -0
- package/build/tools/get-yaml.js +47 -0
- package/build/tools/list-files.d.ts +3 -0
- package/build/tools/list-files.js +30 -0
- package/build/tools/list-pages.d.ts +25 -0
- package/build/tools/list-pages.js +101 -0
- package/build/tools/list-projects.d.ts +3 -0
- package/build/tools/list-projects.js +19 -0
- package/build/tools/sync-project.d.ts +3 -0
- package/build/tools/sync-project.js +144 -0
- package/build/tools/update-yaml.d.ts +3 -0
- package/build/tools/update-yaml.js +24 -0
- package/build/tools/validate-yaml.d.ts +3 -0
- package/build/tools/validate-yaml.js +22 -0
- package/build/utils/cache.d.ts +48 -0
- package/build/utils/cache.js +162 -0
- package/build/utils/decode-yaml.d.ts +7 -0
- package/build/utils/decode-yaml.js +31 -0
- package/build/utils/page-summary/action-summarizer.d.ts +9 -0
- package/build/utils/page-summary/action-summarizer.js +291 -0
- package/build/utils/page-summary/formatter.d.ts +13 -0
- package/build/utils/page-summary/formatter.js +121 -0
- package/build/utils/page-summary/node-extractor.d.ts +17 -0
- package/build/utils/page-summary/node-extractor.js +207 -0
- package/build/utils/page-summary/tree-walker.d.ts +6 -0
- package/build/utils/page-summary/tree-walker.js +55 -0
- package/build/utils/page-summary/types.d.ts +56 -0
- package/build/utils/page-summary/types.js +4 -0
- package/build/utils/parse-folders.d.ts +9 -0
- package/build/utils/parse-folders.js +29 -0
- package/docs/ff-yaml/00-overview.md +137 -0
- package/docs/ff-yaml/01-project-files.md +513 -0
- package/docs/ff-yaml/02-pages.md +572 -0
- package/docs/ff-yaml/03-components.md +413 -0
- package/docs/ff-yaml/04-widgets/README.md +122 -0
- package/docs/ff-yaml/04-widgets/button.md +444 -0
- package/docs/ff-yaml/04-widgets/container.md +358 -0
- package/docs/ff-yaml/04-widgets/dropdown.md +579 -0
- package/docs/ff-yaml/04-widgets/form.md +256 -0
- package/docs/ff-yaml/04-widgets/image.md +276 -0
- package/docs/ff-yaml/04-widgets/layout.md +355 -0
- package/docs/ff-yaml/04-widgets/misc.md +553 -0
- package/docs/ff-yaml/04-widgets/text-field.md +326 -0
- package/docs/ff-yaml/04-widgets/text.md +302 -0
- package/docs/ff-yaml/05-actions.md +843 -0
- package/docs/ff-yaml/06-variables.md +834 -0
- package/docs/ff-yaml/07-data.md +591 -0
- package/docs/ff-yaml/08-custom-code.md +715 -0
- package/docs/ff-yaml/09-theming.md +592 -0
- package/docs/ff-yaml/10-editing-guide.md +454 -0
- package/docs/ff-yaml/README.md +105 -0
- package/package.json +55 -0
- package/skills/ff-widget-patterns.md +141 -0
- 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,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,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;
|