flutterflow-mcp 0.2.2 → 0.2.3
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/build/tools/find-component-usages.d.ts +14 -0
- package/build/tools/find-component-usages.js +2 -2
- package/build/tools/find-page-navigations.d.ts +19 -0
- package/build/tools/find-page-navigations.js +2 -2
- package/build/tools/get-component-summary.js +3 -1
- package/build/tools/get-page-summary.js +3 -1
- package/build/utils/page-summary/action-summarizer.d.ts +16 -1
- package/build/utils/page-summary/action-summarizer.js +3 -3
- package/build/utils/page-summary/formatter.js +9 -1
- package/build/utils/page-summary/node-extractor.d.ts +7 -0
- package/build/utils/page-summary/node-extractor.js +22 -2
- package/build/utils/page-summary/types.d.ts +2 -0
- package/docs/ff-yaml/03-components.md +62 -2
- package/package.json +6 -2
|
@@ -4,4 +4,18 @@
|
|
|
4
4
|
* Zero API calls: everything comes from the local .ff-cache.
|
|
5
5
|
*/
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the value of a parameter pass to a readable string.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveParamValue(paramObj: Record<string, unknown>): string;
|
|
11
|
+
/**
|
|
12
|
+
* Extract parent context (page or component name + ID) from a file key path.
|
|
13
|
+
* Examples:
|
|
14
|
+
* "page/id-Scaffold_xxx/page-widget-tree-outline/node/id-Widget_yyy"
|
|
15
|
+
* "component/id-Container_xxx/component-widget-tree-outline/node/id-Widget_yyy"
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseParentFromKey(fileKey: string): {
|
|
18
|
+
type: "page" | "component";
|
|
19
|
+
id: string;
|
|
20
|
+
} | null;
|
|
7
21
|
export declare function registerFindComponentUsagesTool(server: McpServer): void;
|
|
@@ -18,7 +18,7 @@ async function batchProcess(items, batchSize, fn) {
|
|
|
18
18
|
/**
|
|
19
19
|
* Resolve the value of a parameter pass to a readable string.
|
|
20
20
|
*/
|
|
21
|
-
function resolveParamValue(paramObj) {
|
|
21
|
+
export function resolveParamValue(paramObj) {
|
|
22
22
|
// Check for variable source (e.g. INTERNATIONALIZATION)
|
|
23
23
|
const variable = paramObj.variable;
|
|
24
24
|
if (variable) {
|
|
@@ -60,7 +60,7 @@ function resolveParamValue(paramObj) {
|
|
|
60
60
|
* "page/id-Scaffold_xxx/page-widget-tree-outline/node/id-Widget_yyy"
|
|
61
61
|
* "component/id-Container_xxx/component-widget-tree-outline/node/id-Widget_yyy"
|
|
62
62
|
*/
|
|
63
|
-
function parseParentFromKey(fileKey) {
|
|
63
|
+
export function parseParentFromKey(fileKey) {
|
|
64
64
|
const pageMatch = fileKey.match(/^page\/id-(Scaffold_\w+)\//);
|
|
65
65
|
if (pageMatch)
|
|
66
66
|
return { type: "page", id: pageMatch[1] };
|
|
@@ -4,4 +4,23 @@
|
|
|
4
4
|
* Zero API calls: everything comes from the local .ff-cache.
|
|
5
5
|
*/
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
/**
|
|
8
|
+
* Parse parent context from an action file key.
|
|
9
|
+
* Example: "page/id-Scaffold_XXX/page-widget-tree-outline/node/id-Widget_YYY/trigger_actions/id-ON_TAP/action/id-zzz"
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseActionContext(fileKey: string): {
|
|
12
|
+
parentType: "page" | "component";
|
|
13
|
+
parentId: string;
|
|
14
|
+
widgetKey: string;
|
|
15
|
+
trigger: string;
|
|
16
|
+
} | null;
|
|
17
|
+
/**
|
|
18
|
+
* Recursively search an object for a navigate action targeting the given scaffold ID.
|
|
19
|
+
* Returns navigate details if found, including whether it's inside a disableAction.
|
|
20
|
+
*/
|
|
21
|
+
export declare function findNavigateAction(obj: unknown, targetScaffoldId: string, isDisabled: boolean, depth: number): {
|
|
22
|
+
disabled: boolean;
|
|
23
|
+
allowBack: boolean;
|
|
24
|
+
passedParams: string[];
|
|
25
|
+
} | null;
|
|
7
26
|
export declare function registerFindPageNavigationsTool(server: McpServer): void;
|
|
@@ -18,7 +18,7 @@ async function batchProcess(items, batchSize, fn) {
|
|
|
18
18
|
* Parse parent context from an action file key.
|
|
19
19
|
* Example: "page/id-Scaffold_XXX/page-widget-tree-outline/node/id-Widget_YYY/trigger_actions/id-ON_TAP/action/id-zzz"
|
|
20
20
|
*/
|
|
21
|
-
function parseActionContext(fileKey) {
|
|
21
|
+
export function parseActionContext(fileKey) {
|
|
22
22
|
// Page action
|
|
23
23
|
const pageMatch = fileKey.match(/^page\/id-(Scaffold_\w+)\/.*\/node\/id-(\w+)\/trigger_actions\/id-([^/]+)\/action\//);
|
|
24
24
|
if (pageMatch) {
|
|
@@ -45,7 +45,7 @@ function parseActionContext(fileKey) {
|
|
|
45
45
|
* Recursively search an object for a navigate action targeting the given scaffold ID.
|
|
46
46
|
* Returns navigate details if found, including whether it's inside a disableAction.
|
|
47
47
|
*/
|
|
48
|
-
function findNavigateAction(obj, targetScaffoldId, isDisabled, depth) {
|
|
48
|
+
export function findNavigateAction(obj, targetScaffoldId, isDisabled, depth) {
|
|
49
49
|
if (!obj || typeof obj !== "object" || depth > 12)
|
|
50
50
|
return null;
|
|
51
51
|
const o = obj;
|
|
@@ -110,6 +110,8 @@ outline) {
|
|
|
110
110
|
name: nodeInfo.name,
|
|
111
111
|
slot: outline.slot,
|
|
112
112
|
detail: nodeInfo.detail,
|
|
113
|
+
componentRef: nodeInfo.componentRef,
|
|
114
|
+
componentId: nodeInfo.componentId,
|
|
113
115
|
triggers,
|
|
114
116
|
children,
|
|
115
117
|
};
|
|
@@ -118,7 +120,7 @@ outline) {
|
|
|
118
120
|
// Tool registration
|
|
119
121
|
// ---------------------------------------------------------------------------
|
|
120
122
|
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.", {
|
|
123
|
+
server.tool("get_component_summary", "Get a readable summary of a FlutterFlow component from local cache — widget tree, actions, params. Nested component references are resolved to show [ComponentName] (ComponentId). No API calls. Run sync_project first if not cached.", {
|
|
122
124
|
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
123
125
|
componentName: z
|
|
124
126
|
.string()
|
|
@@ -131,6 +131,8 @@ outline) {
|
|
|
131
131
|
name: nodeInfo.name,
|
|
132
132
|
slot: outline.slot,
|
|
133
133
|
detail: nodeInfo.detail,
|
|
134
|
+
componentRef: nodeInfo.componentRef,
|
|
135
|
+
componentId: nodeInfo.componentId,
|
|
134
136
|
triggers,
|
|
135
137
|
children,
|
|
136
138
|
};
|
|
@@ -139,7 +141,7 @@ outline) {
|
|
|
139
141
|
// Tool registration
|
|
140
142
|
// ---------------------------------------------------------------------------
|
|
141
143
|
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.", {
|
|
144
|
+
server.tool("get_page_summary", "Get a readable summary of a FlutterFlow page from local cache — widget tree, actions, params, state. Component references are resolved to show [ComponentName] (ComponentId) instead of plain Container. Use the ComponentId with get_component_summary to drill into a component. No API calls. Run sync_project first if not cached.", {
|
|
143
145
|
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
144
146
|
pageName: z
|
|
145
147
|
.string()
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import { TriggerSummary } from "./types.js";
|
|
1
|
+
import { ActionSummary, TriggerSummary } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Collect all action keys referenced in a trigger chain.
|
|
4
|
+
* Flattens followUpAction chains, conditionActions, and parallelActions.
|
|
5
|
+
*/
|
|
6
|
+
export declare function collectActionKeys(node: Record<string, unknown>): string[];
|
|
7
|
+
/**
|
|
8
|
+
* Recursively search an object tree for the first recognizable action.
|
|
9
|
+
* Used to unwrap disableAction nodes where the real action is buried
|
|
10
|
+
* inside conditionalActions or other nesting.
|
|
11
|
+
*/
|
|
12
|
+
export declare function findDeepAction(obj: unknown, depth?: number): ActionSummary | null;
|
|
13
|
+
/**
|
|
14
|
+
* Classify an action YAML into a human-readable summary.
|
|
15
|
+
*/
|
|
16
|
+
export declare function classifyAction(doc: Record<string, unknown>): ActionSummary;
|
|
2
17
|
/**
|
|
3
18
|
* Read all trigger_actions for a node and return summaries.
|
|
4
19
|
*
|
|
@@ -10,7 +10,7 @@ import { cacheRead, listCachedKeys } from "../cache.js";
|
|
|
10
10
|
* Collect all action keys referenced in a trigger chain.
|
|
11
11
|
* Flattens followUpAction chains, conditionActions, and parallelActions.
|
|
12
12
|
*/
|
|
13
|
-
function collectActionKeys(node) {
|
|
13
|
+
export function collectActionKeys(node) {
|
|
14
14
|
const keys = [];
|
|
15
15
|
// Direct action reference
|
|
16
16
|
const action = node.action;
|
|
@@ -69,7 +69,7 @@ const ACTION_TYPE_KEYS = [
|
|
|
69
69
|
* Used to unwrap disableAction nodes where the real action is buried
|
|
70
70
|
* inside conditionalActions or other nesting.
|
|
71
71
|
*/
|
|
72
|
-
function findDeepAction(obj, depth = 0) {
|
|
72
|
+
export function findDeepAction(obj, depth = 0) {
|
|
73
73
|
if (!obj || typeof obj !== "object" || depth > 8)
|
|
74
74
|
return null;
|
|
75
75
|
const o = obj;
|
|
@@ -91,7 +91,7 @@ function findDeepAction(obj, depth = 0) {
|
|
|
91
91
|
/**
|
|
92
92
|
* Classify an action YAML into a human-readable summary.
|
|
93
93
|
*/
|
|
94
|
-
function classifyAction(doc) {
|
|
94
|
+
export function classifyAction(doc) {
|
|
95
95
|
// Disabled action — unwrap and recursively find the inner action
|
|
96
96
|
if ("disableAction" in doc) {
|
|
97
97
|
const da = doc.disableAction;
|
|
@@ -19,7 +19,15 @@ function fmtSlot(slot) {
|
|
|
19
19
|
function nodeLabel(node) {
|
|
20
20
|
const parts = [];
|
|
21
21
|
parts.push(fmtSlot(node.slot));
|
|
22
|
-
|
|
22
|
+
if (node.componentRef) {
|
|
23
|
+
parts.push(`[${node.componentRef}]`);
|
|
24
|
+
if (node.componentId) {
|
|
25
|
+
parts.push(` (${node.componentId})`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
parts.push(node.type);
|
|
30
|
+
}
|
|
23
31
|
if (node.name) {
|
|
24
32
|
parts.push(` (${node.name})`);
|
|
25
33
|
}
|
|
@@ -2,7 +2,14 @@ export interface NodeInfo {
|
|
|
2
2
|
type: string;
|
|
3
3
|
name: string;
|
|
4
4
|
detail: string;
|
|
5
|
+
componentRef?: string;
|
|
6
|
+
componentId?: string;
|
|
5
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the inputValue from a FF value object.
|
|
10
|
+
* Returns the literal string, or "[dynamic]" for variable references.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveValue(obj: unknown): string;
|
|
6
13
|
/**
|
|
7
14
|
* Infer widget type from the key prefix (e.g. "Text_abc123" → "Text").
|
|
8
15
|
*/
|
|
@@ -8,7 +8,7 @@ import { cacheRead } from "../cache.js";
|
|
|
8
8
|
* Resolve the inputValue from a FF value object.
|
|
9
9
|
* Returns the literal string, or "[dynamic]" for variable references.
|
|
10
10
|
*/
|
|
11
|
-
function resolveValue(obj) {
|
|
11
|
+
export function resolveValue(obj) {
|
|
12
12
|
if (obj == null)
|
|
13
13
|
return "";
|
|
14
14
|
if (typeof obj === "string" || typeof obj === "number")
|
|
@@ -172,6 +172,18 @@ export function inferTypeFromKey(key) {
|
|
|
172
172
|
const match = key.match(/^([A-Z][a-zA-Z]*)_/);
|
|
173
173
|
return match ? match[1] : "Unknown";
|
|
174
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Resolve a component reference to its human-readable name.
|
|
177
|
+
* Reads the component definition file from cache (e.g. "component/id-Container_xxx").
|
|
178
|
+
*/
|
|
179
|
+
async function resolveComponentName(projectId, componentKey) {
|
|
180
|
+
const compFileKey = `component/id-${componentKey}`;
|
|
181
|
+
const content = await cacheRead(projectId, compFileKey);
|
|
182
|
+
if (!content)
|
|
183
|
+
return undefined;
|
|
184
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
185
|
+
return nameMatch ? nameMatch[1].trim() : undefined;
|
|
186
|
+
}
|
|
175
187
|
/**
|
|
176
188
|
* Read a node's cached file and extract type, name, and detail.
|
|
177
189
|
*
|
|
@@ -195,7 +207,15 @@ export async function extractNodeInfo(projectId, pagePrefix, nodeKey) {
|
|
|
195
207
|
const name = doc.name || "";
|
|
196
208
|
const props = doc.props || {};
|
|
197
209
|
const detail = extractDetail(type, props);
|
|
198
|
-
|
|
210
|
+
// Check for component reference
|
|
211
|
+
const compRef = doc.componentClassKeyRef;
|
|
212
|
+
let componentRef;
|
|
213
|
+
let componentId;
|
|
214
|
+
if (compRef?.key && typeof compRef.key === "string") {
|
|
215
|
+
componentId = compRef.key;
|
|
216
|
+
componentRef = await resolveComponentName(projectId, componentId);
|
|
217
|
+
}
|
|
218
|
+
return { type, name, detail, componentRef, componentId };
|
|
199
219
|
}
|
|
200
220
|
catch {
|
|
201
221
|
return {
|
|
@@ -414,7 +414,67 @@ node:
|
|
|
414
414
|
|
|
415
415
|
---
|
|
416
416
|
|
|
417
|
-
## 10.
|
|
417
|
+
## 10. Components in Summary Output
|
|
418
|
+
|
|
419
|
+
The `get_page_summary` and `get_component_summary` tools resolve component references and display them with a distinct format in the widget tree. This makes it easy to distinguish regular widgets from embedded components at a glance.
|
|
420
|
+
|
|
421
|
+
### Format
|
|
422
|
+
|
|
423
|
+
Component instances appear as `[ComponentName] (Container_ID)` instead of just `Container`:
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
FeedHomePage (Scaffold_e5ows2lg) — folder: home
|
|
427
|
+
|
|
428
|
+
ON_INIT_STATE → [customAction: checkNotificationPermissionResult, ...]
|
|
429
|
+
|
|
430
|
+
Widget Tree:
|
|
431
|
+
└── [body] Container
|
|
432
|
+
└── Column
|
|
433
|
+
└── Column
|
|
434
|
+
├── [Header] (Container_ur4ml9qw)
|
|
435
|
+
├── [SearchBar] (Container_qw4kqc4l)
|
|
436
|
+
└── Container
|
|
437
|
+
└── [PostsList] (Container_pgvko7fz)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Reading the output
|
|
441
|
+
|
|
442
|
+
| Element | Meaning |
|
|
443
|
+
|---|---|
|
|
444
|
+
| `Container`, `Column`, `Row`, etc. | Regular widget — defined inline on this page/component |
|
|
445
|
+
| `[Header] (Container_ur4ml9qw)` | Component instance — `Header` is the component name, `Container_ur4ml9qw` is the component ID |
|
|
446
|
+
| `[body]`, `[appBar]`, etc. | Slot prefix — which slot of the parent widget this node fills |
|
|
447
|
+
| `→ ON_TAP → [navigate: to page]` | Trigger — action(s) attached to this widget |
|
|
448
|
+
|
|
449
|
+
### Drilling into components
|
|
450
|
+
|
|
451
|
+
The component ID in parentheses (e.g. `Container_ur4ml9qw`) can be used to retrieve the full component structure:
|
|
452
|
+
|
|
453
|
+
- **`get_component_summary`** — pass `componentId: "Container_ur4ml9qw"` (or `componentName: "Header"`) to see the component's internal widget tree, params, and actions
|
|
454
|
+
- **`get_project_yaml`** — pass `fileName: "component/id-Container_ur4ml9qw"` to get the raw component metadata YAML
|
|
455
|
+
- **`find_component_usages`** — pass `componentId: "Container_ur4ml9qw"` to find all pages and components where this component is used
|
|
456
|
+
|
|
457
|
+
### Nested components
|
|
458
|
+
|
|
459
|
+
Components can contain other components. The same `[Name] (ID)` format appears at any nesting level:
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
PostsList (Container_pgvko7fz)
|
|
463
|
+
Params: customAudienceFilter (Enum), profileUserId (String), fetchType (Enum), itemId (String)
|
|
464
|
+
|
|
465
|
+
Widget Tree:
|
|
466
|
+
└── ConditionalBuilder
|
|
467
|
+
├── PlaceholderWidget
|
|
468
|
+
│ └── ListView
|
|
469
|
+
│ └── [PostCard] (Container_abc12345) → CALLBACK → [updateState]
|
|
470
|
+
└── PlaceholderWidget
|
|
471
|
+
└── Column
|
|
472
|
+
└── [EmptyState] (Container_xyz98765)
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## 11. Creating a New Component
|
|
418
478
|
|
|
419
479
|
Creating a component requires pushing multiple files in a **single** `update_project_yaml` call, similar to adding widgets to a page.
|
|
420
480
|
|
|
@@ -652,7 +712,7 @@ executeCallbackAction:
|
|
|
652
712
|
|
|
653
713
|
---
|
|
654
714
|
|
|
655
|
-
##
|
|
715
|
+
## 12. Refactoring Page Widgets into a Component
|
|
656
716
|
|
|
657
717
|
To extract existing page widgets into a reusable component:
|
|
658
718
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flutterflow-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "MCP server for the FlutterFlow Project API — AI-assisted FlutterFlow development through Claude and other MCP-compatible clients",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
"build": "tsc && chmod 755 build/index.js",
|
|
17
17
|
"dev": "tsc --watch",
|
|
18
18
|
"start": "node build/index.js",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"test:coverage": "vitest run --coverage",
|
|
19
22
|
"prepublishOnly": "npm run build"
|
|
20
23
|
},
|
|
21
24
|
"keywords": [
|
|
@@ -50,6 +53,7 @@
|
|
|
50
53
|
"devDependencies": {
|
|
51
54
|
"@types/adm-zip": "^0.5.7",
|
|
52
55
|
"@types/node": "^25.2.3",
|
|
53
|
-
"typescript": "^5.9.3"
|
|
56
|
+
"typescript": "^5.9.3",
|
|
57
|
+
"vitest": "^4.0.18"
|
|
54
58
|
}
|
|
55
59
|
}
|