community-ff-mcp 0.4.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 +300 -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 +78 -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 +37 -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 +40 -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 +21 -0
- package/build/tools/find-component-usages.js +216 -0
- package/build/tools/find-page-navigations.d.ts +26 -0
- package/build/tools/find-page-navigations.js +220 -0
- package/build/tools/get-api-endpoints.d.ts +2 -0
- package/build/tools/get-api-endpoints.js +126 -0
- package/build/tools/get-app-settings.d.ts +2 -0
- package/build/tools/get-app-settings.js +169 -0
- package/build/tools/get-app-state.d.ts +2 -0
- package/build/tools/get-app-state.js +96 -0
- package/build/tools/get-component-summary.d.ts +22 -0
- package/build/tools/get-component-summary.js +195 -0
- package/build/tools/get-custom-code.d.ts +2 -0
- package/build/tools/get-custom-code.js +380 -0
- package/build/tools/get-data-models.d.ts +2 -0
- package/build/tools/get-data-models.js +266 -0
- 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-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 +205 -0
- package/build/tools/get-project-config.d.ts +2 -0
- package/build/tools/get-project-config.js +216 -0
- package/build/tools/get-project-setup.d.ts +2 -0
- package/build/tools/get-project-setup.js +212 -0
- package/build/tools/get-theme.d.ts +2 -0
- package/build/tools/get-theme.js +199 -0
- package/build/tools/get-yaml-docs.d.ts +6 -0
- package/build/tools/get-yaml-docs.js +116 -0
- package/build/tools/get-yaml.d.ts +2 -0
- package/build/tools/get-yaml.js +53 -0
- package/build/tools/list-files.d.ts +3 -0
- package/build/tools/list-files.js +49 -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 +23 -0
- package/build/tools/search-project-files.d.ts +2 -0
- package/build/tools/search-project-files.js +69 -0
- package/build/tools/sync-project.d.ts +3 -0
- package/build/tools/sync-project.js +147 -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 +39 -0
- package/build/utils/batch-process.d.ts +2 -0
- package/build/utils/batch-process.js +10 -0
- package/build/utils/cache.d.ts +58 -0
- package/build/utils/cache.js +199 -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 +24 -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 +129 -0
- package/build/utils/page-summary/node-extractor.d.ts +24 -0
- package/build/utils/page-summary/node-extractor.js +227 -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 +58 -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/build/utils/resolve-data-type.d.ts +2 -0
- package/build/utils/resolve-data-type.js +18 -0
- package/build/utils/topic-map.d.ts +7 -0
- package/build/utils/topic-map.js +122 -0
- package/docs/ff-yaml/00-overview.md +166 -0
- package/docs/ff-yaml/01-project-files.md +2309 -0
- package/docs/ff-yaml/02-pages.md +572 -0
- package/docs/ff-yaml/03-components.md +784 -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 +953 -0
- package/docs/ff-yaml/06-variables.md +849 -0
- package/docs/ff-yaml/07-data.md +591 -0
- package/docs/ff-yaml/08-custom-code.md +736 -0
- package/docs/ff-yaml/09-theming.md +638 -0
- package/docs/ff-yaml/10-editing-guide.md +497 -0
- package/docs/ff-yaml/README.md +105 -0
- package/package.json +59 -0
- package/skills/community-ff-mcp/SKILL.md +201 -0
- package/skills/ff-widget-patterns.md +141 -0
- package/skills/ff-yaml-dev.md +70 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
import { cacheRead, cacheMeta, cacheAgeFooter } from "../utils/cache.js";
|
|
4
|
+
function argbToHex(argbStr) {
|
|
5
|
+
const n = parseInt(argbStr, 10);
|
|
6
|
+
if (isNaN(n))
|
|
7
|
+
return argbStr;
|
|
8
|
+
const a = (n >>> 24) & 0xff;
|
|
9
|
+
const r = (n >>> 16) & 0xff;
|
|
10
|
+
const g = (n >>> 8) & 0xff;
|
|
11
|
+
const b = n & 0xff;
|
|
12
|
+
if (a === 255) {
|
|
13
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
|
|
14
|
+
}
|
|
15
|
+
return `#${a.toString(16).padStart(2, "0")}${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
|
|
16
|
+
}
|
|
17
|
+
export function registerGetThemeTool(server) {
|
|
18
|
+
server.tool("get_theme", "Get theme colors, typography, breakpoints, and widget defaults from local cache. No API calls. Run sync_project first if not cached.", {
|
|
19
|
+
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
20
|
+
section: z
|
|
21
|
+
.enum(["colors", "typography", "breakpoints", "widgets", "all"])
|
|
22
|
+
.default("all")
|
|
23
|
+
.describe("Which theme section to return. Defaults to 'all'."),
|
|
24
|
+
}, async ({ projectId, section }) => {
|
|
25
|
+
const meta = await cacheMeta(projectId);
|
|
26
|
+
if (!meta) {
|
|
27
|
+
return {
|
|
28
|
+
content: [
|
|
29
|
+
{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: `No cache found for project "${projectId}". Run sync_project first.`,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const [themeRaw, materialThemeRaw] = await Promise.all([
|
|
37
|
+
cacheRead(projectId, "theme"),
|
|
38
|
+
cacheRead(projectId, "material-theme"),
|
|
39
|
+
]);
|
|
40
|
+
if (!themeRaw) {
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{ type: "text", text: "No theme data found in cache." },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const theme = YAML.parse(themeRaw);
|
|
48
|
+
const sections = ["# Theme"];
|
|
49
|
+
// Material version
|
|
50
|
+
if (materialThemeRaw) {
|
|
51
|
+
const materialTheme = YAML.parse(materialThemeRaw);
|
|
52
|
+
const useMaterial2 = materialTheme.useMaterial2 === true;
|
|
53
|
+
sections.push(`\nMaterial version: Material ${useMaterial2 ? "2" : "3"}`);
|
|
54
|
+
}
|
|
55
|
+
const show = (s) => section === "all" || section === s;
|
|
56
|
+
// --- Colors ---
|
|
57
|
+
if (show("colors")) {
|
|
58
|
+
const colors = extractColors(theme);
|
|
59
|
+
if (colors.length > 0) {
|
|
60
|
+
sections.push("\n## Colors");
|
|
61
|
+
for (const { name, hex } of colors) {
|
|
62
|
+
sections.push(`- ${name}: ${hex}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// --- Typography ---
|
|
67
|
+
if (show("typography")) {
|
|
68
|
+
const defaultTypography = theme.defaultTypography;
|
|
69
|
+
if (defaultTypography) {
|
|
70
|
+
const primaryFont = defaultTypography.primaryFontFamily || "Unknown";
|
|
71
|
+
const secondaryFont = defaultTypography.secondaryFontFamily || "Unknown";
|
|
72
|
+
sections.push("\n## Typography", `Primary font: ${primaryFont}`, `Secondary font: ${secondaryFont}`, "", "| Style | Size | Weight | Color |", "|-------|------|--------|-------|");
|
|
73
|
+
const styleKeys = [
|
|
74
|
+
"displayLarge", "displayMedium", "displaySmall",
|
|
75
|
+
"headlineLarge", "headlineMedium", "headlineSmall",
|
|
76
|
+
"titleLarge", "titleMedium", "titleSmall",
|
|
77
|
+
"bodyLarge", "bodyMedium", "bodySmall",
|
|
78
|
+
"labelLarge", "labelMedium", "labelSmall",
|
|
79
|
+
];
|
|
80
|
+
for (const key of styleKeys) {
|
|
81
|
+
const style = defaultTypography[key];
|
|
82
|
+
if (!style)
|
|
83
|
+
continue;
|
|
84
|
+
const size = style.fontSizeValue?.inputValue ?? "?";
|
|
85
|
+
const weight = style.fontWeightValue?.inputValue ?? "?";
|
|
86
|
+
const color = style.colorValue?.inputValue?.themeColor ?? "?";
|
|
87
|
+
sections.push(`| ${key} | ${size} | ${weight} | ${color} |`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// --- Breakpoints ---
|
|
92
|
+
if (show("breakpoints")) {
|
|
93
|
+
const bp = theme.breakPoints;
|
|
94
|
+
if (bp) {
|
|
95
|
+
const small = bp.small ?? 479;
|
|
96
|
+
const medium = bp.medium ?? 767;
|
|
97
|
+
const large = bp.large ?? 991;
|
|
98
|
+
sections.push("\n## Breakpoints", `- Phone: 0 - ${small}px`, `- Tablet: ${small + 1} - ${medium}px`, `- Tablet landscape: ${medium + 1} - ${large}px`, `- Desktop: ${large + 1}px+`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// --- Widget Defaults ---
|
|
102
|
+
if (show("widgets")) {
|
|
103
|
+
const themeWidgets = theme.themeWidgets;
|
|
104
|
+
if (themeWidgets) {
|
|
105
|
+
sections.push("\n## Widget Defaults");
|
|
106
|
+
for (const [widgetName, widget] of Object.entries(themeWidgets)) {
|
|
107
|
+
sections.push(`\n### ${widgetName}`);
|
|
108
|
+
formatWidgetDefaults(widget, sections);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const scrollbar = theme.scrollbarTheme;
|
|
112
|
+
if (scrollbar) {
|
|
113
|
+
sections.push("\n### scrollbarTheme");
|
|
114
|
+
if (scrollbar.thickness !== undefined)
|
|
115
|
+
sections.push(`Thickness: ${scrollbar.thickness}`);
|
|
116
|
+
if (scrollbar.radius !== undefined)
|
|
117
|
+
sections.push(`Radius: ${scrollbar.radius}`);
|
|
118
|
+
const thumbColor = scrollbar.thumbColor;
|
|
119
|
+
if (thumbColor?.value) {
|
|
120
|
+
sections.push(`Thumb color: ${argbToHex(thumbColor.value)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: "text", text: sections.join("\n") + cacheAgeFooter(meta) }],
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function extractColors(theme) {
|
|
130
|
+
const results = [];
|
|
131
|
+
for (const [key, val] of Object.entries(theme)) {
|
|
132
|
+
if (val &&
|
|
133
|
+
typeof val === "object" &&
|
|
134
|
+
!Array.isArray(val)) {
|
|
135
|
+
const obj = val;
|
|
136
|
+
// Check if this is a single color entry: { value: "<argb>" }
|
|
137
|
+
if (typeof obj.value === "string" &&
|
|
138
|
+
Object.keys(obj).length <= 2 &&
|
|
139
|
+
isArgbColor(obj.value)) {
|
|
140
|
+
results.push({ name: key, hex: argbToHex(obj.value) });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
// Check if this is a map of colors: { COLOR_NAME: { value: "<argb>" }, ... }
|
|
144
|
+
const childEntries = Object.entries(obj);
|
|
145
|
+
const colorChildren = [];
|
|
146
|
+
for (const [childKey, childVal] of childEntries) {
|
|
147
|
+
if (childVal &&
|
|
148
|
+
typeof childVal === "object" &&
|
|
149
|
+
!Array.isArray(childVal)) {
|
|
150
|
+
const child = childVal;
|
|
151
|
+
if (typeof child.value === "string" && isArgbColor(child.value)) {
|
|
152
|
+
colorChildren.push({ name: childKey, hex: argbToHex(child.value) });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (colorChildren.length >= 3) {
|
|
157
|
+
results.push(...colorChildren);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return results;
|
|
162
|
+
}
|
|
163
|
+
function isArgbColor(value) {
|
|
164
|
+
const n = parseInt(value, 10);
|
|
165
|
+
return !isNaN(n) && n > 0xff000000;
|
|
166
|
+
}
|
|
167
|
+
function formatWidgetDefaults(widget, out) {
|
|
168
|
+
if (widget.fontFamily)
|
|
169
|
+
out.push(`Font: ${widget.fontFamily}`);
|
|
170
|
+
const textStyle = widget.textStyle;
|
|
171
|
+
if (textStyle?.themeStyle)
|
|
172
|
+
out.push(`Text style: ${textStyle.themeStyle}`);
|
|
173
|
+
const textColor = widget.textColorValue;
|
|
174
|
+
const textColorInput = textColor?.inputValue;
|
|
175
|
+
if (textColorInput?.themeColor)
|
|
176
|
+
out.push(`Text color: ${textColorInput.themeColor}`);
|
|
177
|
+
const bgColor = widget.backgroundColorValue;
|
|
178
|
+
const bgInput = bgColor?.inputValue;
|
|
179
|
+
if (bgInput?.themeColor)
|
|
180
|
+
out.push(`Background: ${bgInput.themeColor}`);
|
|
181
|
+
const borderRadius = widget.borderRadius;
|
|
182
|
+
if (borderRadius) {
|
|
183
|
+
const allVal = borderRadius.allValue;
|
|
184
|
+
if (allVal?.inputValue !== undefined) {
|
|
185
|
+
out.push(`Border radius: ${allVal.inputValue}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const dimensions = widget.dimensions;
|
|
189
|
+
if (dimensions) {
|
|
190
|
+
const height = dimensions.height;
|
|
191
|
+
const heightPx = height?.pixelsValue;
|
|
192
|
+
if (heightPx?.inputValue !== undefined)
|
|
193
|
+
out.push(`Height: ${heightPx.inputValue}px`);
|
|
194
|
+
const width = dimensions.width;
|
|
195
|
+
const widthPx = width?.pixelsValue;
|
|
196
|
+
if (widthPx?.inputValue !== undefined)
|
|
197
|
+
out.push(`Width: ${widthPx.inputValue}px`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -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,116 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TOPIC_MAP, DOCS_DIR, listDocFiles, readDoc } from "../utils/topic-map.js";
|
|
3
|
+
export function registerGetYamlDocsTool(server) {
|
|
4
|
+
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.", {
|
|
5
|
+
topic: z
|
|
6
|
+
.string()
|
|
7
|
+
.optional()
|
|
8
|
+
.describe("Search topic/keyword (e.g. 'Button', 'actions', 'theme', 'Column', 'variables'). Case-insensitive."),
|
|
9
|
+
file: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("Specific doc file path (e.g. '04-widgets/button', '05-actions', 'README'). Omit .md extension."),
|
|
13
|
+
}, async ({ topic, file }) => {
|
|
14
|
+
// Direct file access
|
|
15
|
+
if (file) {
|
|
16
|
+
const content = readDoc(`${file}.md`);
|
|
17
|
+
if (content) {
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: "text", text: content }],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const available = listDocFiles(DOCS_DIR)
|
|
23
|
+
.map((f) => ` - ${f.replace(/\.md$/, "")}`)
|
|
24
|
+
.join("\n");
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: `Doc file not found: "${file}"\n\nAvailable files:\n${available}`,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Topic search
|
|
35
|
+
if (topic) {
|
|
36
|
+
const key = topic.toLowerCase().replace(/[\s_-]+/g, "");
|
|
37
|
+
const matchedFile = TOPIC_MAP[key];
|
|
38
|
+
if (matchedFile) {
|
|
39
|
+
const content = readDoc(matchedFile);
|
|
40
|
+
if (content) {
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: `# Matched: ${matchedFile}\n\n${content}`,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Fallback: scan filenames and file contents for the topic
|
|
52
|
+
const allFiles = listDocFiles(DOCS_DIR);
|
|
53
|
+
const matches = [];
|
|
54
|
+
for (const f of allFiles) {
|
|
55
|
+
// Check filename match
|
|
56
|
+
if (f.toLowerCase().includes(topic.toLowerCase())) {
|
|
57
|
+
matches.push(f);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// Check content match (first-level scan)
|
|
61
|
+
const content = readDoc(f);
|
|
62
|
+
if (content &&
|
|
63
|
+
content.toLowerCase().includes(topic.toLowerCase())) {
|
|
64
|
+
matches.push(f);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (matches.length === 0) {
|
|
68
|
+
const available = allFiles
|
|
69
|
+
.map((f) => ` - ${f.replace(/\.md$/, "")}`)
|
|
70
|
+
.join("\n");
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: `No docs found for topic "${topic}".\n\nAvailable docs:\n${available}`,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Return the first match content, list others
|
|
81
|
+
const primary = readDoc(matches[0]) || "";
|
|
82
|
+
const others = matches.length > 1
|
|
83
|
+
? `\n\n---\n\nOther matching docs:\n${matches
|
|
84
|
+
.slice(1)
|
|
85
|
+
.map((f) => ` - ${f.replace(/\.md$/, "")}`)
|
|
86
|
+
.join("\n")}`
|
|
87
|
+
: "";
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: `# Matched: ${matches[0]}\n\n${primary}${others}`,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// No params: return index
|
|
98
|
+
const readme = readDoc("README.md");
|
|
99
|
+
if (readme) {
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: "text", text: readme }],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const available = listDocFiles(DOCS_DIR)
|
|
105
|
+
.map((f) => ` - ${f.replace(/\.md$/, "")}`)
|
|
106
|
+
.join("\n");
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: `FlutterFlow YAML Reference Docs:\n${available}\n\nUse the 'topic' or 'file' parameter to fetch specific docs.`,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { cacheRead, listCachedKeys } from "../utils/cache.js";
|
|
3
|
+
export function registerGetYamlTool(server) {
|
|
4
|
+
server.tool("get_project_yaml", "Read YAML files from the local project cache. Requires sync_project to be run first. Returns one file if fileName is specified, or lists all cached file keys if omitted.", {
|
|
5
|
+
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
6
|
+
fileName: z
|
|
7
|
+
.string()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe("Specific YAML file name to read (e.g. 'app-details', 'page/id-xxx'). Omit to list all cached file keys."),
|
|
10
|
+
}, async ({ projectId, fileName }) => {
|
|
11
|
+
if (fileName) {
|
|
12
|
+
const cached = await cacheRead(projectId, fileName);
|
|
13
|
+
if (cached) {
|
|
14
|
+
return {
|
|
15
|
+
content: [
|
|
16
|
+
{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: `# ${fileName}\n${cached}`,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
content: [
|
|
25
|
+
{
|
|
26
|
+
type: "text",
|
|
27
|
+
text: `File "${fileName}" not found in local cache for project "${projectId}". Run sync_project(projectId: "${projectId}") first to download all YAML files, then retry.`,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// No fileName: list all cached keys
|
|
33
|
+
const keys = await listCachedKeys(projectId);
|
|
34
|
+
if (keys.length === 0) {
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: "text",
|
|
39
|
+
text: `No cached files found for project "${projectId}". Run sync_project(projectId: "${projectId}") first to download all YAML files.`,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: `# Cached files (${keys.length})\n${keys.join("\n")}`,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { listCachedKeys, cacheMeta, cacheAgeFooter } 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. Supports optional prefix filter (e.g. 'page/', 'component/') to narrow results.", {
|
|
5
|
+
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
6
|
+
prefix: z.string().optional().describe("Optional prefix to filter file keys (e.g. 'page/', 'custom-file/')"),
|
|
7
|
+
}, async ({ projectId, prefix }) => {
|
|
8
|
+
// If cache exists, return file keys from cache
|
|
9
|
+
const meta = await cacheMeta(projectId);
|
|
10
|
+
if (meta) {
|
|
11
|
+
const keys = prefix
|
|
12
|
+
? await listCachedKeys(projectId, prefix)
|
|
13
|
+
: await listCachedKeys(projectId);
|
|
14
|
+
return {
|
|
15
|
+
content: [
|
|
16
|
+
{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: JSON.stringify({ value: { file_names: keys }, source: "cache", syncedAt: meta.lastSyncedAt }, null, 2) + cacheAgeFooter(meta),
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const result = await client.listPartitionedFileNames(projectId);
|
|
24
|
+
// Apply client-side prefix filter when falling back to API
|
|
25
|
+
if (prefix) {
|
|
26
|
+
const apiResult = result;
|
|
27
|
+
const allKeys = apiResult?.value?.file_names;
|
|
28
|
+
if (Array.isArray(allKeys)) {
|
|
29
|
+
const filtered = allKeys.filter((k) => k.startsWith(prefix));
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: "text",
|
|
34
|
+
text: JSON.stringify({ value: { file_names: filtered } }, null, 2),
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: JSON.stringify(result, null, 2),
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { decodeProjectYamlResponse } from "../utils/decode-yaml.js";
|
|
3
|
+
import { parseFolderMapping } from "../utils/parse-folders.js";
|
|
4
|
+
import { cacheRead, cacheWrite } from "../utils/cache.js";
|
|
5
|
+
/**
|
|
6
|
+
* Extract page scaffold IDs from the partitioned file names list.
|
|
7
|
+
* Filters for top-level page files only (not sub-files like widget-tree-outline).
|
|
8
|
+
*/
|
|
9
|
+
export function extractPageFileKeys(fileNames) {
|
|
10
|
+
const raw = fileNames;
|
|
11
|
+
const names = raw?.value?.file_names ?? raw?.value?.fileNames ?? [];
|
|
12
|
+
return names.filter((n) => /^page\/id-Scaffold_\w+$/.test(n));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Fetch a single YAML file and decode it. Returns null on failure.
|
|
16
|
+
*/
|
|
17
|
+
export async function fetchOneFile(client, projectId, fileName) {
|
|
18
|
+
// Check cache first
|
|
19
|
+
const cached = await cacheRead(projectId, fileName);
|
|
20
|
+
if (cached) {
|
|
21
|
+
return { fileKey: fileName, content: cached };
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const raw = await client.getProjectYamls(projectId, fileName);
|
|
25
|
+
const decoded = decodeProjectYamlResponse(raw);
|
|
26
|
+
const entries = Object.entries(decoded);
|
|
27
|
+
if (entries.length > 0) {
|
|
28
|
+
// Write to cache on fetch
|
|
29
|
+
await cacheWrite(projectId, fileName, entries[0][1]);
|
|
30
|
+
return { fileKey: fileName, content: entries[0][1] };
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Process items in batches to avoid API rate limits.
|
|
40
|
+
*/
|
|
41
|
+
async function batchProcess(items, batchSize, fn) {
|
|
42
|
+
const results = [];
|
|
43
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
44
|
+
const batch = items.slice(i, i + batchSize);
|
|
45
|
+
const batchResults = await Promise.allSettled(batch.map(fn));
|
|
46
|
+
results.push(...batchResults);
|
|
47
|
+
}
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Extract a compact page index by fetching pages in batches.
|
|
52
|
+
*/
|
|
53
|
+
export async function listPages(client, projectId) {
|
|
54
|
+
// Step 1: Get file list and folders in parallel
|
|
55
|
+
const [fileNamesRaw, foldersResult] = await Promise.all([
|
|
56
|
+
client.listPartitionedFileNames(projectId),
|
|
57
|
+
fetchOneFile(client, projectId, "folders"),
|
|
58
|
+
]);
|
|
59
|
+
const pageFileKeys = extractPageFileKeys(fileNamesRaw);
|
|
60
|
+
const folderMap = foldersResult
|
|
61
|
+
? parseFolderMapping(foldersResult.content)
|
|
62
|
+
: {};
|
|
63
|
+
// Step 2: Fetch pages in batches of 5 to avoid rate limits
|
|
64
|
+
const results = await batchProcess(pageFileKeys, 5, (fileKey) => fetchOneFile(client, projectId, fileKey));
|
|
65
|
+
const pages = [];
|
|
66
|
+
for (let i = 0; i < pageFileKeys.length; i++) {
|
|
67
|
+
const fileKey = pageFileKeys[i];
|
|
68
|
+
const scaffoldMatch = fileKey.match(/^page\/id-(Scaffold_\w+)$/);
|
|
69
|
+
if (!scaffoldMatch)
|
|
70
|
+
continue;
|
|
71
|
+
const scaffoldId = scaffoldMatch[1];
|
|
72
|
+
const result = results[i];
|
|
73
|
+
let name = "(error - could not fetch)";
|
|
74
|
+
if (result.status === "fulfilled" && result.value) {
|
|
75
|
+
const nameMatch = result.value.content.match(/^name:\s*(.+)$/m);
|
|
76
|
+
name = nameMatch ? nameMatch[1].trim() : "(unknown)";
|
|
77
|
+
}
|
|
78
|
+
const folder = folderMap[scaffoldId] || "(unmapped)";
|
|
79
|
+
pages.push({ scaffoldId, name, folder, fileKey });
|
|
80
|
+
}
|
|
81
|
+
// Sort by folder then name
|
|
82
|
+
pages.sort((a, b) => a.folder === b.folder
|
|
83
|
+
? a.name.localeCompare(b.name)
|
|
84
|
+
: a.folder.localeCompare(b.folder));
|
|
85
|
+
return pages;
|
|
86
|
+
}
|
|
87
|
+
export function registerListPagesTool(server, client) {
|
|
88
|
+
server.tool("list_pages", "List all pages in a FlutterFlow project with human-readable names, scaffold IDs, and folder assignments. Use this FIRST to discover pages before fetching their YAML.", {
|
|
89
|
+
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
90
|
+
}, async ({ projectId }) => {
|
|
91
|
+
const pages = await listPages(client, projectId);
|
|
92
|
+
return {
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: "text",
|
|
96
|
+
text: JSON.stringify(pages, null, 2),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerListProjectsTool(server, client) {
|
|
3
|
+
server.tool("list_projects", "List FlutterFlow projects for the authenticated user. NOTE: This may not return all projects you have access to (shared/team projects can be missing). If a project is missing, copy its ID directly from the FlutterFlow editor (click the project name in the top-left corner).", {
|
|
4
|
+
project_type: z
|
|
5
|
+
.string()
|
|
6
|
+
.optional()
|
|
7
|
+
.describe("Optional filter for project type"),
|
|
8
|
+
}, async ({ project_type }) => {
|
|
9
|
+
const result = await client.listProjects(project_type);
|
|
10
|
+
return {
|
|
11
|
+
content: [
|
|
12
|
+
{
|
|
13
|
+
type: "text",
|
|
14
|
+
text: JSON.stringify(result, null, 2),
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: "\n---\n**Tip:** This list may not include all projects you have access to (shared/team projects can be missing). If you don't see a project here, copy its ID directly from the FlutterFlow editor: click the project name (top-left corner) and copy the project ID.",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { cacheMeta, listCachedKeys, cacheAgeFooter } from "../utils/cache.js";
|
|
3
|
+
const MAX_RESULTS = 100;
|
|
4
|
+
export function registerSearchProjectFilesTool(server) {
|
|
5
|
+
server.tool("search_project_files", "Search cached project file keys by keyword, prefix, or regex. Returns matching file keys for use with get_project_yaml. Cache-only, no API calls. Run sync_project first.", {
|
|
6
|
+
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
7
|
+
query: z.string().describe("Search query string"),
|
|
8
|
+
mode: z
|
|
9
|
+
.enum(["prefix", "contains", "regex"])
|
|
10
|
+
.default("contains")
|
|
11
|
+
.describe("Search mode: prefix (startsWith), contains (case-insensitive includes), or regex (case-insensitive RegExp)"),
|
|
12
|
+
}, async ({ projectId, query, mode }) => {
|
|
13
|
+
const meta = await cacheMeta(projectId);
|
|
14
|
+
if (!meta) {
|
|
15
|
+
return {
|
|
16
|
+
content: [
|
|
17
|
+
{
|
|
18
|
+
type: "text",
|
|
19
|
+
text: `No cache found for project "${projectId}". Run sync_project first.`,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const allKeys = await listCachedKeys(projectId);
|
|
25
|
+
let matched;
|
|
26
|
+
switch (mode) {
|
|
27
|
+
case "prefix":
|
|
28
|
+
matched = allKeys.filter((k) => k.startsWith(query));
|
|
29
|
+
break;
|
|
30
|
+
case "regex": {
|
|
31
|
+
const re = new RegExp(query, "i");
|
|
32
|
+
matched = allKeys.filter((k) => re.test(k));
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case "contains":
|
|
36
|
+
default: {
|
|
37
|
+
const lowerQuery = query.toLowerCase();
|
|
38
|
+
matched = allKeys.filter((k) => k.toLowerCase().includes(lowerQuery));
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (matched.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "text",
|
|
47
|
+
text: `No files found matching "${query}" (mode: ${mode}).`,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const total = matched.length;
|
|
53
|
+
const truncated = total > MAX_RESULTS;
|
|
54
|
+
const displayed = truncated ? matched.slice(0, MAX_RESULTS) : matched;
|
|
55
|
+
const lines = [];
|
|
56
|
+
if (truncated) {
|
|
57
|
+
lines.push(`Found ${MAX_RESULTS} of ${total} files matching "${query}" (showing first ${MAX_RESULTS}):`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
lines.push(`Found ${total} files matching "${query}":`);
|
|
61
|
+
}
|
|
62
|
+
for (const key of displayed) {
|
|
63
|
+
lines.push(`- ${key}`);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: lines.join("\n") + cacheAgeFooter(meta) }],
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|