flutterflow-mcp 0.2.1 → 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.
@@ -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
- parts.push(node.type);
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
- return { type, name, detail };
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 {
@@ -18,6 +18,8 @@ export interface SummaryNode {
18
18
  name: string;
19
19
  slot: string;
20
20
  detail: string;
21
+ componentRef?: string;
22
+ componentId?: string;
21
23
  triggers: TriggerSummary[];
22
24
  children: SummaryNode[];
23
25
  }
@@ -414,7 +414,67 @@ node:
414
414
 
415
415
  ---
416
416
 
417
- ## 10. Creating a New Component
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
- ## 11. Refactoring Page Widgets into a Component
715
+ ## 12. Refactoring Page Widgets into a Component
656
716
 
657
717
  To extract existing page widgets into a reusable component:
658
718
 
@@ -726,7 +726,112 @@ trueAction:
726
726
 
727
727
  ## Disabled Actions
728
728
 
729
- Actions can be conditionally disabled using a `disableAction` wrapper. When the condition evaluates to true, the wrapped action executes; the chain terminates otherwise.
729
+ FlutterFlow uses `disableAction` to mark actions or conditionals as disabled. There are three distinct patterns depending on what is being disabled.
730
+
731
+ ### Pattern 1: Disabling a Leaf Action (Unconditional)
732
+
733
+ To fully disable a single action (navigate, database, revenueCat, etc.), wrap the action content in `disableAction` at the **action file level**. The trigger chain continues to reference the same key — FlutterFlow reads the file, sees `disableAction`, and skips execution.
734
+
735
+ ```yaml
736
+ # Action file: id-epxy2a0w.yaml
737
+ key: epxy2a0w
738
+ disableAction:
739
+ actionNode:
740
+ key: gm8dis03 # Internal node key (can be any unique key)
741
+ action:
742
+ navigate:
743
+ allowBack: false
744
+ passedParameters:
745
+ widgetClassNodeKeyRef:
746
+ key: Scaffold_tydsj8ql
747
+ isNavigateBack: false
748
+ transition:
749
+ transitionType: FADE_IN
750
+ durationMillis: 500
751
+ pageNodeKeyRef:
752
+ key: Scaffold_tydsj8ql
753
+ key: epxy2a0w # Must match the outer key
754
+ ```
755
+
756
+ | Field | Description |
757
+ |-------|-------------|
758
+ | `key` (outer) | The action's original key — chain references this |
759
+ | `disableAction.actionNode.key` | Internal node key (any unique value) |
760
+ | `disableAction.actionNode.action` | The original action content, preserved for re-enabling |
761
+ | `disableAction.actionNode.action.key` | Must match the outer `key` |
762
+
763
+ **Important:** Disabling a leaf action at the file level does NOT disable `followUpAction` chains defined in the trigger YAML. If the trigger chain has a `followUpAction` after this action reference, that follow-up will still execute.
764
+
765
+ ### Pattern 2: Disabling a Conditional Node
766
+
767
+ To disable an entire `conditionActions` block in the trigger chain, two changes are required:
768
+
769
+ **Step 1 — Create a new action file** that wraps the full conditional in `disableAction.actionNode.conditionActions`. Leaf actions within the conditional are inlined (not file references) with their own `disableAction` wrappers:
770
+
771
+ ```yaml
772
+ # Action file: id-ds8cnd01.yaml (new file)
773
+ key: ds8cnd01
774
+ disableAction:
775
+ actionNode:
776
+ key: f441awwi # Original conditional node key from the chain
777
+ conditionActions:
778
+ falseAction:
779
+ key: wkklfcpy
780
+ action:
781
+ key: epxy2a0w
782
+ disableAction: # Leaf action inlined with its own disable
783
+ actionNode:
784
+ key: gm8dis03
785
+ action:
786
+ navigate:
787
+ allowBack: false
788
+ pageNodeKeyRef:
789
+ key: Scaffold_tydsj8ql
790
+ key: epxy2a0w
791
+ trueActions:
792
+ - condition:
793
+ revenueCatEntitlementResponse: {}
794
+ trueAction:
795
+ key: 5uq5p5vt
796
+ action:
797
+ key: bgxsmj4b
798
+ disableAction: # Another leaf action inlined
799
+ actionNode:
800
+ key: gm8dis04
801
+ action:
802
+ navigate:
803
+ allowBack: false
804
+ pageNodeKeyRef:
805
+ key: Scaffold_91ca3wwv
806
+ key: bgxsmj4b
807
+ hasMultiConditions: false
808
+ key: ev7ush66
809
+ ```
810
+
811
+ **Step 2 — Update the trigger chain YAML.** Replace the `conditionActions` block with an `action` reference to a noop key (a key with no corresponding action file):
812
+
813
+ ```yaml
814
+ # Before (in trigger chain):
815
+ followUpAction:
816
+ key: f441awwi
817
+ conditionActions:
818
+ falseAction: ...
819
+ trueActions: ...
820
+ hasMultiConditions: false
821
+ key: ev7ush66
822
+
823
+ # After (in trigger chain):
824
+ followUpAction:
825
+ key: f441awwi
826
+ action:
827
+ key: np8noop1 # Noop key — no action file exists for this key
828
+ ```
829
+
830
+ **Note:** The noop key has no corresponding action file. When pushed via the UI, FlutterFlow handles this internally. When pushed via the API, you may need to create the action file with the disabled conditional content under the noop key itself (see API Limitations).
831
+
832
+ ### Pattern 3: Conditionally Disabled Action
833
+
834
+ Actions can be conditionally disabled using `disableAction` with a condition. When the condition evaluates to true, the wrapped action executes; otherwise it is skipped.
730
835
 
731
836
  ```yaml
732
837
  key: hfzr0kmk
@@ -763,7 +868,12 @@ disableAction:
763
868
  key: xsa3loej
764
869
  ```
765
870
 
766
- Note: In `disableAction`, the action body can be inlined directly rather than referenced by key.
871
+ ### Key Rules
872
+
873
+ - `disableAction` is only valid in **action files**, never in the trigger chain YAML directly
874
+ - Leaf actions inlined inside `disableAction` use the full action body, not key references
875
+ - The original action content is preserved inside the wrapper for easy re-enabling
876
+ - Disabling a leaf action does NOT prevent `followUpAction` chains in the trigger from continuing
767
877
 
768
878
  ---
769
879
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flutterflow-mcp",
3
- "version": "0.2.1",
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
  }