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.
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flutterflow-mcp",
3
- "version": "0.2.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
  }