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,225 @@
1
+ import { z } from "zod";
2
+ import YAML from "yaml";
3
+ import { cacheRead, cacheMeta, listCachedKeys, } from "../utils/cache.js";
4
+ import { resolveComponent } from "./get-component-summary.js";
5
+ // ---------------------------------------------------------------------------
6
+ // Helpers
7
+ // ---------------------------------------------------------------------------
8
+ /** Batch-process items in groups to avoid overwhelming the file system. */
9
+ async function batchProcess(items, batchSize, fn) {
10
+ const results = [];
11
+ for (let i = 0; i < items.length; i += batchSize) {
12
+ const batch = items.slice(i, i + batchSize);
13
+ const batchResults = await Promise.all(batch.map(fn));
14
+ results.push(...batchResults);
15
+ }
16
+ return results;
17
+ }
18
+ /**
19
+ * Resolve the value of a parameter pass to a readable string.
20
+ */
21
+ function resolveParamValue(paramObj) {
22
+ // Check for variable source (e.g. INTERNATIONALIZATION)
23
+ const variable = paramObj.variable;
24
+ if (variable) {
25
+ const source = variable.source;
26
+ if (source === "INTERNATIONALIZATION") {
27
+ const fc = variable.functionCall;
28
+ const values = fc?.values;
29
+ if (values && values.length > 0) {
30
+ const first = values[0];
31
+ const iv = first.inputValue;
32
+ const sv = iv?.serializedValue;
33
+ return sv ? `"${sv}" (i18n)` : "[i18n]";
34
+ }
35
+ return "[i18n]";
36
+ }
37
+ if (source)
38
+ return `[${source}]`;
39
+ return "[dynamic]";
40
+ }
41
+ // Check for inputValue
42
+ const inputValue = paramObj.inputValue;
43
+ if (inputValue != null) {
44
+ if (typeof inputValue === "string" || typeof inputValue === "number") {
45
+ return `"${inputValue}"`;
46
+ }
47
+ if (typeof inputValue === "object") {
48
+ const ivo = inputValue;
49
+ if ("serializedValue" in ivo)
50
+ return `"${ivo.serializedValue}"`;
51
+ if ("themeColor" in ivo)
52
+ return `[theme:${ivo.themeColor}]`;
53
+ }
54
+ }
55
+ return "[dynamic]";
56
+ }
57
+ /**
58
+ * Extract parent context (page or component name + ID) from a file key path.
59
+ * Examples:
60
+ * "page/id-Scaffold_xxx/page-widget-tree-outline/node/id-Widget_yyy"
61
+ * "component/id-Container_xxx/component-widget-tree-outline/node/id-Widget_yyy"
62
+ */
63
+ function parseParentFromKey(fileKey) {
64
+ const pageMatch = fileKey.match(/^page\/id-(Scaffold_\w+)\//);
65
+ if (pageMatch)
66
+ return { type: "page", id: pageMatch[1] };
67
+ const compMatch = fileKey.match(/^component\/id-(Container_\w+)\//);
68
+ if (compMatch)
69
+ return { type: "component", id: compMatch[1] };
70
+ return null;
71
+ }
72
+ /**
73
+ * Resolve the name of a page or component from its top-level YAML cache.
74
+ */
75
+ async function resolveParentName(projectId, parentType, parentId) {
76
+ const prefix = parentType === "page" ? "page" : "component";
77
+ const fileKey = `${prefix}/id-${parentId}`;
78
+ const content = await cacheRead(projectId, fileKey);
79
+ if (!content)
80
+ return parentId;
81
+ const nameMatch = content.match(/^name:\s*(.+)$/m);
82
+ return nameMatch ? nameMatch[1].trim() : parentId;
83
+ }
84
+ // ---------------------------------------------------------------------------
85
+ // Tool registration
86
+ // ---------------------------------------------------------------------------
87
+ export function registerFindComponentUsagesTool(server) {
88
+ server.tool("find_component_usages", "Find all pages and components where a given component is used, with parameter pass details. Cache-only, no API calls. Run sync_project first.", {
89
+ projectId: z.string().describe("The FlutterFlow project ID"),
90
+ componentName: z
91
+ .string()
92
+ .optional()
93
+ .describe("Human-readable component name (e.g. 'PremuimContentWall'). Case-insensitive."),
94
+ componentId: z
95
+ .string()
96
+ .optional()
97
+ .describe("Container ID (e.g. 'Container_ffzg5wc5')."),
98
+ }, async ({ projectId, componentName, componentId }) => {
99
+ // Check cache exists
100
+ const meta = await cacheMeta(projectId);
101
+ if (!meta) {
102
+ return {
103
+ content: [
104
+ {
105
+ type: "text",
106
+ text: `No cache found for project "${projectId}". Run sync_project first to download the project YAML files.`,
107
+ },
108
+ ],
109
+ };
110
+ }
111
+ if (!componentName && !componentId) {
112
+ return {
113
+ content: [
114
+ {
115
+ type: "text",
116
+ text: "Provide either componentName or componentId.",
117
+ },
118
+ ],
119
+ };
120
+ }
121
+ // Resolve component to its key
122
+ const resolved = await resolveComponent(projectId, componentName, componentId);
123
+ if (!resolved.ok) {
124
+ const list = resolved.available.map((n) => ` - ${n}`).join("\n");
125
+ const searchTerm = componentName || componentId || "";
126
+ return {
127
+ content: [
128
+ {
129
+ type: "text",
130
+ text: `Component "${searchTerm}" not found in cache. Available components:\n${list}`,
131
+ },
132
+ ],
133
+ };
134
+ }
135
+ const targetId = resolved.component.containerId;
136
+ // Get component name for display
137
+ const componentContent = await cacheRead(projectId, resolved.component.componentFileKey);
138
+ const displayName = componentContent
139
+ ? (componentContent.match(/^name:\s*(.+)$/m)?.[1]?.trim() || targetId)
140
+ : targetId;
141
+ // Scan all node files for componentClassKeyRef matching this component
142
+ const allKeys = await listCachedKeys(projectId);
143
+ const nodeKeys = allKeys.filter((k) => /\/node\/id-[A-Z]/.test(k));
144
+ // Read node files in batches to check for component references
145
+ const usages = [];
146
+ await batchProcess(nodeKeys, 20, async (nodeKey) => {
147
+ const content = await cacheRead(projectId, nodeKey);
148
+ if (!content)
149
+ return;
150
+ // Quick string check before parsing YAML
151
+ if (!content.includes(targetId))
152
+ return;
153
+ let doc;
154
+ try {
155
+ doc = YAML.parse(content);
156
+ }
157
+ catch {
158
+ return;
159
+ }
160
+ // Check componentClassKeyRef
161
+ const classKeyRef = doc.componentClassKeyRef;
162
+ if (!classKeyRef)
163
+ return;
164
+ const refKey = classKeyRef.key;
165
+ if (refKey !== targetId)
166
+ return;
167
+ // Found a usage! Extract parent context
168
+ const parent = parseParentFromKey(nodeKey);
169
+ if (!parent)
170
+ return;
171
+ // Extract the widget key from the file key
172
+ const widgetKeyMatch = nodeKey.match(/\/node\/id-(\w+)$/);
173
+ const widgetKey = widgetKeyMatch ? widgetKeyMatch[1] : "unknown";
174
+ // Extract parameter passes
175
+ const params = [];
176
+ const parameterValues = doc.parameterValues;
177
+ if (parameterValues) {
178
+ for (const [, paramVal] of Object.entries(parameterValues)) {
179
+ const paramId = paramVal.paramIdentifier;
180
+ const value = resolveParamValue(paramVal);
181
+ params.push({
182
+ paramName: paramId || "unknown",
183
+ value,
184
+ });
185
+ }
186
+ }
187
+ // Resolve parent name
188
+ const parentName = await resolveParentName(projectId, parent.type, parent.id);
189
+ usages.push({
190
+ parentType: parent.type,
191
+ parentName,
192
+ parentId: parent.id,
193
+ widgetKey,
194
+ params,
195
+ });
196
+ });
197
+ // Format output
198
+ if (usages.length === 0) {
199
+ return {
200
+ content: [
201
+ {
202
+ type: "text",
203
+ text: `Component: ${displayName} (${targetId})\nNo usages found in cached files.`,
204
+ },
205
+ ],
206
+ };
207
+ }
208
+ const lines = [];
209
+ lines.push(`Component: ${displayName} (${targetId})`);
210
+ lines.push(`Found ${usages.length} usage${usages.length === 1 ? "" : "s"}:`);
211
+ lines.push("");
212
+ for (let i = 0; i < usages.length; i++) {
213
+ const u = usages[i];
214
+ const parentLabel = u.parentType === "page" ? u.parentName : `[component] ${u.parentName}`;
215
+ lines.push(`${i + 1}. ${parentLabel} (${u.parentId}) \u2192 ${u.widgetKey}`);
216
+ if (u.params.length > 0) {
217
+ const paramStrs = u.params.map((p) => `${p.paramName} = ${p.value}`);
218
+ lines.push(` Params: ${paramStrs.join(", ")}`);
219
+ }
220
+ }
221
+ return {
222
+ content: [{ type: "text", text: lines.join("\n") }],
223
+ };
224
+ });
225
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * find_page_navigations tool — scans cached action files to find all places
3
+ * that navigate to a given page.
4
+ * Zero API calls: everything comes from the local .ff-cache.
5
+ */
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ export declare function registerFindPageNavigationsTool(server: McpServer): void;
@@ -0,0 +1,228 @@
1
+ import { z } from "zod";
2
+ import YAML from "yaml";
3
+ import { cacheRead, cacheMeta, listCachedKeys, } from "../utils/cache.js";
4
+ import { resolvePage } from "./get-page-summary.js";
5
+ // ---------------------------------------------------------------------------
6
+ // Helpers
7
+ // ---------------------------------------------------------------------------
8
+ async function batchProcess(items, batchSize, fn) {
9
+ const results = [];
10
+ for (let i = 0; i < items.length; i += batchSize) {
11
+ const batch = items.slice(i, i + batchSize);
12
+ const batchResults = await Promise.all(batch.map(fn));
13
+ results.push(...batchResults);
14
+ }
15
+ return results;
16
+ }
17
+ /**
18
+ * Parse parent context from an action file key.
19
+ * Example: "page/id-Scaffold_XXX/page-widget-tree-outline/node/id-Widget_YYY/trigger_actions/id-ON_TAP/action/id-zzz"
20
+ */
21
+ function parseActionContext(fileKey) {
22
+ // Page action
23
+ const pageMatch = fileKey.match(/^page\/id-(Scaffold_\w+)\/.*\/node\/id-(\w+)\/trigger_actions\/id-([^/]+)\/action\//);
24
+ if (pageMatch) {
25
+ return {
26
+ parentType: "page",
27
+ parentId: pageMatch[1],
28
+ widgetKey: pageMatch[2],
29
+ trigger: pageMatch[3],
30
+ };
31
+ }
32
+ // Component action
33
+ const compMatch = fileKey.match(/^component\/id-(Container_\w+)\/.*\/node\/id-(\w+)\/trigger_actions\/id-([^/]+)\/action\//);
34
+ if (compMatch) {
35
+ return {
36
+ parentType: "component",
37
+ parentId: compMatch[1],
38
+ widgetKey: compMatch[2],
39
+ trigger: compMatch[3],
40
+ };
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * Recursively search an object for a navigate action targeting the given scaffold ID.
46
+ * Returns navigate details if found, including whether it's inside a disableAction.
47
+ */
48
+ function findNavigateAction(obj, targetScaffoldId, isDisabled, depth) {
49
+ if (!obj || typeof obj !== "object" || depth > 12)
50
+ return null;
51
+ const o = obj;
52
+ // Check for disableAction wrapper
53
+ if ("disableAction" in o) {
54
+ const da = o.disableAction;
55
+ return findNavigateAction(da, targetScaffoldId, true, depth + 1);
56
+ }
57
+ // Check for navigate action with matching pageNodeKeyRef
58
+ if ("navigate" in o) {
59
+ const nav = o.navigate;
60
+ const pageRef = nav.pageNodeKeyRef;
61
+ if (pageRef?.key === targetScaffoldId) {
62
+ const allowBack = nav.allowBack ?? true;
63
+ const passedParams = [];
64
+ const params = nav.passedParameters;
65
+ if (params) {
66
+ for (const [key, val] of Object.entries(params)) {
67
+ if (key === "widgetClassNodeKeyRef")
68
+ continue;
69
+ passedParams.push(key);
70
+ }
71
+ }
72
+ return { disabled: isDisabled, allowBack, passedParams };
73
+ }
74
+ }
75
+ // Recurse into values
76
+ for (const val of Object.values(o)) {
77
+ if (val && typeof val === "object") {
78
+ const found = findNavigateAction(val, targetScaffoldId, isDisabled, depth + 1);
79
+ if (found)
80
+ return found;
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+ /**
86
+ * Resolve the name of a page or component from its top-level YAML cache.
87
+ */
88
+ async function resolveParentName(projectId, parentType, parentId) {
89
+ const prefix = parentType === "page" ? "page" : "component";
90
+ const fileKey = `${prefix}/id-${parentId}`;
91
+ const content = await cacheRead(projectId, fileKey);
92
+ if (!content)
93
+ return parentId;
94
+ const nameMatch = content.match(/^name:\s*(.+)$/m);
95
+ return nameMatch ? nameMatch[1].trim() : parentId;
96
+ }
97
+ // ---------------------------------------------------------------------------
98
+ // Tool registration
99
+ // ---------------------------------------------------------------------------
100
+ export function registerFindPageNavigationsTool(server) {
101
+ server.tool("find_page_navigations", "Find all actions that navigate to a given page, showing source page, trigger, disabled status, and passed parameters. Cache-only, no API calls. Run sync_project first.", {
102
+ projectId: z.string().describe("The FlutterFlow project ID"),
103
+ pageName: z
104
+ .string()
105
+ .optional()
106
+ .describe("Human-readable page name (e.g. 'PaywallPage'). Case-insensitive."),
107
+ scaffoldId: z
108
+ .string()
109
+ .optional()
110
+ .describe("Scaffold ID (e.g. 'Scaffold_tydsj8ql')."),
111
+ }, async ({ projectId, pageName, scaffoldId }) => {
112
+ // Check cache exists
113
+ const meta = await cacheMeta(projectId);
114
+ if (!meta) {
115
+ return {
116
+ content: [
117
+ {
118
+ type: "text",
119
+ text: `No cache found for project "${projectId}". Run sync_project first to download the project YAML files.`,
120
+ },
121
+ ],
122
+ };
123
+ }
124
+ if (!pageName && !scaffoldId) {
125
+ return {
126
+ content: [
127
+ {
128
+ type: "text",
129
+ text: "Provide either pageName or scaffoldId.",
130
+ },
131
+ ],
132
+ };
133
+ }
134
+ // Resolve page
135
+ const resolved = await resolvePage(projectId, pageName, scaffoldId);
136
+ if (!resolved.ok) {
137
+ const list = resolved.available.map((n) => ` - ${n}`).join("\n");
138
+ const searchTerm = pageName || scaffoldId || "";
139
+ return {
140
+ content: [
141
+ {
142
+ type: "text",
143
+ text: `Page "${searchTerm}" not found in cache. Available pages:\n${list}`,
144
+ },
145
+ ],
146
+ };
147
+ }
148
+ const targetScaffoldId = resolved.page.scaffoldId;
149
+ // Get target page name for display
150
+ const targetContent = await cacheRead(projectId, resolved.page.pageFileKey);
151
+ const displayName = targetContent
152
+ ? (targetContent.match(/^name:\s*(.+)$/m)?.[1]?.trim() || targetScaffoldId)
153
+ : targetScaffoldId;
154
+ // Scan all action files for navigate references
155
+ const allKeys = await listCachedKeys(projectId);
156
+ const actionKeys = allKeys.filter((k) => /\/action\/id-/.test(k));
157
+ const refs = [];
158
+ await batchProcess(actionKeys, 20, async (actionKey) => {
159
+ const content = await cacheRead(projectId, actionKey);
160
+ if (!content)
161
+ return;
162
+ // Quick string check before YAML parsing
163
+ if (!content.includes(targetScaffoldId))
164
+ return;
165
+ let doc;
166
+ try {
167
+ doc = YAML.parse(content);
168
+ }
169
+ catch {
170
+ return;
171
+ }
172
+ const navInfo = findNavigateAction(doc, targetScaffoldId, false, 0);
173
+ if (!navInfo)
174
+ return;
175
+ const ctx = parseActionContext(actionKey);
176
+ if (!ctx)
177
+ return;
178
+ const parentName = await resolveParentName(projectId, ctx.parentType, ctx.parentId);
179
+ refs.push({
180
+ parentType: ctx.parentType,
181
+ parentName,
182
+ parentId: ctx.parentId,
183
+ widgetKey: ctx.widgetKey,
184
+ trigger: ctx.trigger,
185
+ disabled: navInfo.disabled,
186
+ allowBack: navInfo.allowBack,
187
+ passedParams: navInfo.passedParams,
188
+ });
189
+ });
190
+ // Deduplicate (same parent + widget + trigger)
191
+ const seen = new Set();
192
+ const uniqueRefs = refs.filter((r) => {
193
+ const key = `${r.parentId}:${r.widgetKey}:${r.trigger}:${r.disabled}`;
194
+ if (seen.has(key))
195
+ return false;
196
+ seen.add(key);
197
+ return true;
198
+ });
199
+ // Format output
200
+ if (uniqueRefs.length === 0) {
201
+ return {
202
+ content: [
203
+ {
204
+ type: "text",
205
+ text: `Page: ${displayName} (${targetScaffoldId})\nNo navigations found in cached action files.`,
206
+ },
207
+ ],
208
+ };
209
+ }
210
+ const lines = [];
211
+ lines.push(`Page: ${displayName} (${targetScaffoldId})`);
212
+ lines.push(`Found ${uniqueRefs.length} navigation${uniqueRefs.length === 1 ? "" : "s"}:`);
213
+ lines.push("");
214
+ for (let i = 0; i < uniqueRefs.length; i++) {
215
+ const r = uniqueRefs[i];
216
+ const status = r.disabled ? "[DISABLED] " : "";
217
+ const parentLabel = r.parentType === "page" ? r.parentName : `[component] ${r.parentName}`;
218
+ const back = r.allowBack ? "" : " (no back)";
219
+ lines.push(`${i + 1}. ${status}${parentLabel} (${r.parentId}) → ${r.trigger} on ${r.widgetKey}${back}`);
220
+ if (r.passedParams.length > 0) {
221
+ lines.push(` Params: ${r.passedParams.join(", ")}`);
222
+ }
223
+ }
224
+ return {
225
+ content: [{ type: "text", text: lines.join("\n") }],
226
+ };
227
+ });
228
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * get_component_summary tool — assembles cached sub-files into a readable
3
+ * component summary. Zero API calls: everything comes from the local .ff-cache.
4
+ */
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ interface ResolvedComponent {
7
+ containerId: string;
8
+ componentFileKey: string;
9
+ }
10
+ /**
11
+ * Resolve a component name or container ID to its cache file key.
12
+ * Returns all available component names if no match found.
13
+ */
14
+ export declare function resolveComponent(projectId: string, componentName?: string, componentId?: string): Promise<{
15
+ ok: true;
16
+ component: ResolvedComponent;
17
+ } | {
18
+ ok: false;
19
+ available: string[];
20
+ }>;
21
+ export declare function registerGetComponentSummaryTool(server: McpServer): void;
22
+ export {};
@@ -0,0 +1,193 @@
1
+ import { z } from "zod";
2
+ import YAML from "yaml";
3
+ import { cacheRead, cacheMeta, listCachedKeys, } from "../utils/cache.js";
4
+ import { parseTreeOutline } from "../utils/page-summary/tree-walker.js";
5
+ import { extractNodeInfo } from "../utils/page-summary/node-extractor.js";
6
+ import { summarizeTriggers } from "../utils/page-summary/action-summarizer.js";
7
+ import { formatComponentSummary } from "../utils/page-summary/formatter.js";
8
+ /**
9
+ * Resolve a component name or container ID to its cache file key.
10
+ * Returns all available component names if no match found.
11
+ */
12
+ export async function resolveComponent(projectId, componentName, componentId) {
13
+ if (componentId) {
14
+ const componentFileKey = `component/id-${componentId}`;
15
+ const content = await cacheRead(projectId, componentFileKey);
16
+ if (content) {
17
+ return { ok: true, component: { containerId: componentId, componentFileKey } };
18
+ }
19
+ }
20
+ // List all cached component top-level files
21
+ const allKeys = await listCachedKeys(projectId, "component/id-");
22
+ // Filter to top-level component files only (not sub-files)
23
+ const componentKeys = allKeys.filter((k) => /^component\/id-Container_\w+$/.test(k));
24
+ const available = [];
25
+ for (const key of componentKeys) {
26
+ const content = await cacheRead(projectId, key);
27
+ if (!content)
28
+ continue;
29
+ const nameMatch = content.match(/^name:\s*(.+)$/m);
30
+ const name = nameMatch ? nameMatch[1].trim() : "";
31
+ if (componentName && name.toLowerCase() === componentName.toLowerCase()) {
32
+ const cid = key.match(/^component\/id-(Container_\w+)$/)?.[1] || "";
33
+ return { ok: true, component: { containerId: cid, componentFileKey: key } };
34
+ }
35
+ if (name)
36
+ available.push(name);
37
+ }
38
+ return { ok: false, available };
39
+ }
40
+ // ---------------------------------------------------------------------------
41
+ // Metadata extraction
42
+ // ---------------------------------------------------------------------------
43
+ /** Resolve a scalar/list data type to a readable string. */
44
+ function resolveDataType(dt) {
45
+ if (dt.listType) {
46
+ const inner = dt.listType;
47
+ return `List<${inner.scalarType || "unknown"}>`;
48
+ }
49
+ if (dt.scalarType === "DataStruct") {
50
+ const sub = dt.subType;
51
+ const dsi = sub?.dataStructIdentifier;
52
+ return dsi?.name ? `DataStruct:${dsi.name}` : "DataStruct";
53
+ }
54
+ if (dt.enumType) {
55
+ const en = dt.enumType;
56
+ const eid = en.enumIdentifier;
57
+ return eid?.name ? `Enum:${eid.name}` : "Enum";
58
+ }
59
+ return dt.scalarType || "unknown";
60
+ }
61
+ /** Extract component metadata (name, description, params) from the top-level YAML. */
62
+ async function extractComponentMeta(projectId, component) {
63
+ const content = await cacheRead(projectId, component.componentFileKey);
64
+ if (!content) {
65
+ return {
66
+ componentName: component.containerId,
67
+ containerId: component.containerId,
68
+ description: "",
69
+ params: [],
70
+ };
71
+ }
72
+ const doc = YAML.parse(content);
73
+ const componentName = doc.name || component.containerId;
74
+ const description = doc.description || "";
75
+ // Params
76
+ const params = [];
77
+ const rawParams = doc.params;
78
+ if (rawParams) {
79
+ for (const val of Object.values(rawParams)) {
80
+ const id = val.identifier;
81
+ const name = id?.name || "unknown";
82
+ const dt = val.dataType || {};
83
+ const defaultVal = val.defaultValue;
84
+ params.push({
85
+ name,
86
+ dataType: resolveDataType(dt),
87
+ defaultValue: defaultVal?.serializedValue,
88
+ });
89
+ }
90
+ }
91
+ return { componentName, containerId: component.containerId, description, params };
92
+ }
93
+ // ---------------------------------------------------------------------------
94
+ // Tree enrichment (reused from page summary)
95
+ // ---------------------------------------------------------------------------
96
+ /**
97
+ * Recursively enrich an OutlineNode tree with node info and trigger summaries.
98
+ */
99
+ async function enrichNode(projectId, treePrefix, // e.g. "component/id-Container_xxx/component-widget-tree-outline"
100
+ outline) {
101
+ const nodeInfo = await extractNodeInfo(projectId, treePrefix, outline.key);
102
+ // Check for triggers
103
+ const nodeFilePrefix = `${treePrefix}/node/id-${outline.key}`;
104
+ const triggers = await summarizeTriggers(projectId, nodeFilePrefix);
105
+ // Enrich children
106
+ const children = await Promise.all(outline.children.map((child) => enrichNode(projectId, treePrefix, child)));
107
+ return {
108
+ key: outline.key,
109
+ type: nodeInfo.type,
110
+ name: nodeInfo.name,
111
+ slot: outline.slot,
112
+ detail: nodeInfo.detail,
113
+ triggers,
114
+ children,
115
+ };
116
+ }
117
+ // ---------------------------------------------------------------------------
118
+ // Tool registration
119
+ // ---------------------------------------------------------------------------
120
+ export function registerGetComponentSummaryTool(server) {
121
+ server.tool("get_component_summary", "Get a readable summary of a FlutterFlow component from local cache — widget tree, actions, params. No API calls. Run sync_project first if not cached.", {
122
+ projectId: z.string().describe("The FlutterFlow project ID"),
123
+ componentName: z
124
+ .string()
125
+ .optional()
126
+ .describe("Human-readable component name (e.g. 'PremuimContentWall'). Case-insensitive. Provide either componentName or componentId."),
127
+ componentId: z
128
+ .string()
129
+ .optional()
130
+ .describe("Container ID (e.g. 'Container_ffzg5wc5'). Provide either componentName or componentId."),
131
+ }, async ({ projectId, componentName, componentId }) => {
132
+ // Check cache exists
133
+ const meta = await cacheMeta(projectId);
134
+ if (!meta) {
135
+ return {
136
+ content: [
137
+ {
138
+ type: "text",
139
+ text: `No cache found for project "${projectId}". Run sync_project first to download the project YAML files.`,
140
+ },
141
+ ],
142
+ };
143
+ }
144
+ if (!componentName && !componentId) {
145
+ return {
146
+ content: [
147
+ {
148
+ type: "text",
149
+ text: "Provide either componentName or componentId.",
150
+ },
151
+ ],
152
+ };
153
+ }
154
+ // Resolve component
155
+ const resolved = await resolveComponent(projectId, componentName, componentId);
156
+ if (!resolved.ok) {
157
+ const list = resolved.available.map((n) => ` - ${n}`).join("\n");
158
+ const searchTerm = componentName || componentId || "";
159
+ return {
160
+ content: [
161
+ {
162
+ type: "text",
163
+ text: `Component "${searchTerm}" not found in cache. Available components:\n${list}`,
164
+ },
165
+ ],
166
+ };
167
+ }
168
+ const component = resolved.component;
169
+ // Extract metadata
170
+ const componentMeta = await extractComponentMeta(projectId, component);
171
+ // Parse widget tree outline
172
+ const outlineKey = `${component.componentFileKey}/component-widget-tree-outline`;
173
+ const outlineContent = await cacheRead(projectId, outlineKey);
174
+ if (!outlineContent) {
175
+ return {
176
+ content: [
177
+ {
178
+ type: "text",
179
+ text: `Component "${componentMeta.componentName}" found but widget tree outline is not cached. Re-run sync_project to fetch all sub-files.`,
180
+ },
181
+ ],
182
+ };
183
+ }
184
+ const outlineTree = parseTreeOutline(outlineContent);
185
+ // Enrich the tree with node info and triggers
186
+ const enrichedTree = await enrichNode(projectId, outlineKey, outlineTree);
187
+ // Format output
188
+ const summary = formatComponentSummary(componentMeta, enrichedTree);
189
+ return {
190
+ content: [{ type: "text", text: summary }],
191
+ };
192
+ });
193
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { FlutterFlowClient } from "../api/flutterflow.js";
3
+ export declare function registerGetPageByNameTool(server: McpServer, client: FlutterFlowClient): void;