flutterflow-mcp 0.1.0 → 0.2.1

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/index.js CHANGED
@@ -29,7 +29,7 @@ const client = createClient();
29
29
  // Register tools
30
30
  registerListProjectsTool(server, client);
31
31
  registerListFilesTool(server, client);
32
- registerGetYamlTool(server, client);
32
+ registerGetYamlTool(server);
33
33
  registerValidateYamlTool(server, client);
34
34
  registerUpdateYamlTool(server, client);
35
35
  registerListPagesTool(server, client);
@@ -72,6 +72,15 @@ const TOPIC_MAP = {
72
72
  scaffold: "02-pages.md",
73
73
  components: "03-components.md",
74
74
  component: "03-components.md",
75
+ createcomponent: "03-components.md",
76
+ refactor: "03-components.md",
77
+ refactoring: "03-components.md",
78
+ isdummyroot: "03-components.md",
79
+ dummyroot: "03-components.md",
80
+ componentclasskeyref: "03-components.md",
81
+ parametervalues: "03-components.md",
82
+ callback: "03-components.md",
83
+ executecallbackaction: "03-components.md",
75
84
  // Universal patterns
76
85
  inputvalue: "README.md",
77
86
  mostrecentinputvalue: "README.md",
@@ -1,3 +1,2 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { FlutterFlowClient } from "../api/flutterflow.js";
3
- export declare function registerGetYamlTool(server: McpServer, client: FlutterFlowClient): void;
2
+ export declare function registerGetYamlTool(server: McpServer): void;
@@ -1,15 +1,13 @@
1
1
  import { z } from "zod";
2
- import { decodeProjectYamlResponse } from "../utils/decode-yaml.js";
3
- import { cacheRead, cacheWrite } from "../utils/cache.js";
4
- export function registerGetYamlTool(server, client) {
5
- server.tool("get_project_yaml", "Download YAML files from a FlutterFlow project. Returns one file if fileName is specified, otherwise returns all files.", {
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.", {
6
5
  projectId: z.string().describe("The FlutterFlow project ID"),
7
6
  fileName: z
8
7
  .string()
9
8
  .optional()
10
- .describe("Specific YAML file name to download (e.g. 'app-details', 'page/id-xxx'). Omit to get all files."),
9
+ .describe("Specific YAML file name to read (e.g. 'app-details', 'page/id-xxx'). Omit to list all cached file keys."),
11
10
  }, async ({ projectId, fileName }) => {
12
- // Cache-first for single-file requests
13
11
  if (fileName) {
14
12
  const cached = await cacheRead(projectId, fileName);
15
13
  if (cached) {
@@ -17,31 +15,39 @@ export function registerGetYamlTool(server, client) {
17
15
  content: [
18
16
  {
19
17
  type: "text",
20
- text: `# ${fileName} (cached)\n${cached}`,
18
+ text: `# ${fileName}\n${cached}`,
21
19
  },
22
20
  ],
23
21
  };
24
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
+ };
25
31
  }
26
- const raw = await client.getProjectYamls(projectId, fileName);
27
- const decoded = decodeProjectYamlResponse(raw);
28
- // Write fetched results to cache (strip .yaml from ZIP entry names to avoid double extension)
29
- for (const [name, yaml] of Object.entries(decoded)) {
30
- const cleanName = name.endsWith(".yaml") ? name.slice(0, -".yaml".length) : name;
31
- await cacheWrite(projectId, cleanName, yaml);
32
- }
33
- const entries = Object.entries(decoded);
34
- if (entries.length === 1) {
35
- const [name, yaml] = entries[0];
32
+ // No fileName: list all cached keys
33
+ const keys = await listCachedKeys(projectId);
34
+ if (keys.length === 0) {
36
35
  return {
37
- content: [{ type: "text", text: `# ${name}\n${yaml}` }],
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
+ ],
38
42
  };
39
43
  }
40
44
  return {
41
- content: entries.map(([name, yaml]) => ({
42
- type: "text",
43
- text: `# ${name}\n${yaml}`,
44
- })),
45
+ content: [
46
+ {
47
+ type: "text",
48
+ text: `# Cached files (${keys.length})\n${keys.join("\n")}`,
49
+ },
50
+ ],
45
51
  };
46
52
  });
47
53
  }
@@ -411,3 +411,294 @@ node:
411
411
  - **Component node files** follow the exact same structure as page node files. The only difference is the file key prefix (`component/` vs `page/`).
412
412
  - **The tree outline key** is `component-widget-tree-outline` (not `page-widget-tree-outline`).
413
413
  - **CALLBACK triggers** appear when an Action parameter is executed from a child widget that is itself a component reference. The trigger key includes the param key (e.g., `CALLBACK-bins86`).
414
+
415
+ ---
416
+
417
+ ## 10. Creating a New Component
418
+
419
+ Creating a component requires pushing multiple files in a **single** `update_project_yaml` call, similar to adding widgets to a page.
420
+
421
+ ### Required files
422
+
423
+ | # | File Key | Purpose |
424
+ |---|----------|---------|
425
+ | 1 | `component/id-Container_XXX` | Component metadata (name, params, state) |
426
+ | 2 | `component/id-Container_XXX/component-widget-tree-outline` | Widget tree hierarchy |
427
+ | 3 | `component/id-Container_XXX/component-widget-tree-outline/node/id-Container_XXX` | Root Container node (`isDummyRoot: true`) |
428
+ | 4 | `component/id-Container_XXX/component-widget-tree-outline/node/id-Widget_YYY` | One file per child widget |
429
+
430
+ ### Step-by-step example
431
+
432
+ **Task:** Create a reusable `DealCard` component with `title` (String), `subtitle` (String), and `onTap` (Action) parameters.
433
+
434
+ **File 1 — Component metadata:**
435
+ ```yaml
436
+ # File key: component/id-Container_dc01root
437
+ name: DealCard
438
+ description: "Reusable card displaying a deal with title, subtitle, and tap action"
439
+ params:
440
+ p1titl:
441
+ identifier:
442
+ name: title
443
+ key: p1titl
444
+ dataType:
445
+ scalarType: String
446
+ nonNullable: true
447
+ p2subt:
448
+ identifier:
449
+ name: subtitle
450
+ key: p2subt
451
+ dataType:
452
+ scalarType: String
453
+ nonNullable: false
454
+ p3tap:
455
+ identifier:
456
+ name: onTap
457
+ key: p3tap
458
+ dataType:
459
+ scalarType: Action
460
+ nonNullable: false
461
+ node:
462
+ key: Container_dc01root
463
+ ```
464
+
465
+ **File 2 — Widget tree outline:**
466
+ ```yaml
467
+ # File key: component/id-Container_dc01root/component-widget-tree-outline
468
+ node:
469
+ key: Container_dc01root
470
+ children:
471
+ - key: Container_dc01card
472
+ children:
473
+ - key: Column_dc01col
474
+ children:
475
+ - key: Text_dc01titl
476
+ - key: Text_dc01sub
477
+ ```
478
+
479
+ **File 3 — Root Container node:**
480
+ ```yaml
481
+ # File key: component/id-Container_dc01root/component-widget-tree-outline/node/id-Container_dc01root
482
+ key: Container_dc01root
483
+ type: Container
484
+ props:
485
+ container:
486
+ boxDecoration:
487
+ colorValue:
488
+ inputValue:
489
+ value: "0"
490
+ mostRecentInputValue:
491
+ value: "0"
492
+ name: DealCard
493
+ isDummyRoot: true
494
+ ```
495
+
496
+ > The root Container must have `isDummyRoot: true` and a transparent background (`value: "0"`). This marks it as the component boundary — the actual visual content starts with the first child.
497
+
498
+ **File 4 — Card container node:**
499
+ ```yaml
500
+ # File key: component/id-Container_dc01root/component-widget-tree-outline/node/id-Container_dc01card
501
+ key: Container_dc01card
502
+ type: Container
503
+ props:
504
+ container:
505
+ boxDecoration:
506
+ colorValue:
507
+ inputValue:
508
+ themeColor: SECONDARY_BACKGROUND
509
+ mostRecentInputValue:
510
+ themeColor: SECONDARY_BACKGROUND
511
+ borderRadiusValue:
512
+ inputValue: 12
513
+ mostRecentInputValue: 12
514
+ paddingValue:
515
+ inputValue: "16,16,16,16"
516
+ mostRecentInputValue: "16,16,16,16"
517
+ widthValue:
518
+ inputValue: "double.infinity"
519
+ mostRecentInputValue: "double.infinity"
520
+ responsiveVisibility: {}
521
+ parameterValues: {}
522
+ valueKey: {}
523
+ ```
524
+
525
+ **File 5 — Column node:**
526
+ ```yaml
527
+ # File key: component/id-Container_dc01root/component-widget-tree-outline/node/id-Column_dc01col
528
+ key: Column_dc01col
529
+ type: Column
530
+ props:
531
+ column:
532
+ crossAxisAlignmentValue:
533
+ inputValue: START
534
+ mostRecentInputValue: START
535
+ responsiveVisibility: {}
536
+ parameterValues: {}
537
+ valueKey: {}
538
+ ```
539
+
540
+ **File 6 — Title text (references component parameter):**
541
+ ```yaml
542
+ # File key: component/id-Container_dc01root/component-widget-tree-outline/node/id-Text_dc01titl
543
+ key: Text_dc01titl
544
+ type: Text
545
+ props:
546
+ text:
547
+ themeStyle: TITLE_MEDIUM
548
+ selectable: false
549
+ textValue:
550
+ variable:
551
+ source: WIDGET_CLASS_PARAMETER
552
+ baseVariable:
553
+ widgetClass:
554
+ paramIdentifier:
555
+ name: title
556
+ key: p1titl
557
+ nodeKeyRef:
558
+ key: Container_dc01root
559
+ colorValue:
560
+ inputValue:
561
+ themeColor: PRIMARY_TEXT
562
+ fontWeightValue:
563
+ inputValue: w600
564
+ responsiveVisibility: {}
565
+ parameterValues: {}
566
+ valueKey: {}
567
+ ```
568
+
569
+ > To display a component parameter, use `source: WIDGET_CLASS_PARAMETER` with `baseVariable.widgetClass.paramIdentifier` pointing to the param key. `nodeKeyRef.key` must point to the component's root Container ID.
570
+
571
+ **File 7 — Subtitle text:**
572
+ ```yaml
573
+ # File key: component/id-Container_dc01root/component-widget-tree-outline/node/id-Text_dc01sub
574
+ key: Text_dc01sub
575
+ type: Text
576
+ props:
577
+ text:
578
+ themeStyle: BODY_MEDIUM
579
+ selectable: false
580
+ textValue:
581
+ variable:
582
+ source: WIDGET_CLASS_PARAMETER
583
+ baseVariable:
584
+ widgetClass:
585
+ paramIdentifier:
586
+ name: subtitle
587
+ key: p2subt
588
+ nodeKeyRef:
589
+ key: Container_dc01root
590
+ colorValue:
591
+ inputValue:
592
+ themeColor: SECONDARY_TEXT
593
+ responsiveVisibility: {}
594
+ parameterValues: {}
595
+ valueKey: {}
596
+ ```
597
+
598
+ **Push all files in one call:**
599
+ ```
600
+ update_project_yaml(projectId, {
601
+ "component/id-Container_dc01root": metadataYaml,
602
+ "component/id-Container_dc01root/component-widget-tree-outline": treeOutlineYaml,
603
+ "component/id-Container_dc01root/component-widget-tree-outline/node/id-Container_dc01root": rootNodeYaml,
604
+ "component/id-Container_dc01root/component-widget-tree-outline/node/id-Container_dc01card": cardNodeYaml,
605
+ "component/id-Container_dc01root/component-widget-tree-outline/node/id-Column_dc01col": columnNodeYaml,
606
+ "component/id-Container_dc01root/component-widget-tree-outline/node/id-Text_dc01titl": titleNodeYaml,
607
+ "component/id-Container_dc01root/component-widget-tree-outline/node/id-Text_dc01sub": subtitleNodeYaml
608
+ })
609
+ ```
610
+
611
+ ### Using the new component in a page
612
+
613
+ After creating the component, embed it in a page by adding a Container node with `componentClassKeyRef`:
614
+
615
+ ```yaml
616
+ # Node in the host page
617
+ key: Container_hostref1
618
+ type: Container
619
+ props:
620
+ expanded:
621
+ expandedType: UNEXPANDED
622
+ responsiveVisibility: {}
623
+ parameterValues:
624
+ parameterPasses:
625
+ p1titl:
626
+ paramIdentifier:
627
+ name: title
628
+ key: p1titl
629
+ inputValue:
630
+ serializedValue: "50% Off Coffee"
631
+ p2subt:
632
+ paramIdentifier:
633
+ name: subtitle
634
+ key: p2subt
635
+ inputValue:
636
+ serializedValue: "Valid until end of month"
637
+ widgetClassNodeKeyRef:
638
+ key: Container_dc01root
639
+ componentClassKeyRef:
640
+ key: Container_dc01root
641
+ ```
642
+
643
+ For Action-type parameters, use `executeCallbackAction` inside the component:
644
+ ```yaml
645
+ # Action inside the component that invokes the onTap callback
646
+ key: act01tap
647
+ executeCallbackAction:
648
+ parameterIdentifier:
649
+ name: onTap
650
+ key: p3tap
651
+ ```
652
+
653
+ ---
654
+
655
+ ## 11. Refactoring Page Widgets into a Component
656
+
657
+ To extract existing page widgets into a reusable component:
658
+
659
+ ### Step 1: Identify the widget subtree to extract
660
+
661
+ Use `get_page_summary` or `get_page_by_name` to find the subtree you want to extract. Note the root widget key and all descendant keys.
662
+
663
+ ### Step 2: Decide on component parameters
664
+
665
+ Any data that was previously hardcoded or came from page state/params needs to become a component parameter. Common patterns:
666
+
667
+ | Was | Becomes |
668
+ |-----|---------|
669
+ | Hardcoded text | String parameter |
670
+ | Page state variable | Parameter passed from page |
671
+ | Page parameter | Parameter passed from page |
672
+ | Navigation action | Action (callback) parameter |
673
+ | API call result | Parameter or kept internal |
674
+
675
+ ### Step 3: Create component files
676
+
677
+ 1. **Metadata file** — Define `name`, `params`, and optionally `classModel.stateFields`
678
+ 2. **Tree outline** — Copy the subtree from the page's `page-widget-tree-outline`, wrapping it under a new root Container with `isDummyRoot: true`
679
+ 3. **Root node** — Create the `isDummyRoot: true` Container
680
+ 4. **Child nodes** — Copy existing node files from the page, changing file key prefix from `page/id-Scaffold_XXX/page-widget-tree-outline/node/` to `component/id-Container_XXX/component-widget-tree-outline/node/`
681
+ 5. **Update data references** — Replace hardcoded values with `WIDGET_CLASS_PARAMETER` variable references
682
+
683
+ ### Step 4: Update the page
684
+
685
+ 1. **Remove extracted nodes** from the page's tree outline
686
+ 2. **Replace with a component reference** — Add a single Container node with `componentClassKeyRef` pointing to the new component
687
+ 3. **Pass parameters** via `parameterValues.parameterPasses`
688
+
689
+ ### Step 5: Push everything in one call
690
+
691
+ Push all component files AND the updated page files in a single `update_project_yaml` call to avoid an inconsistent state.
692
+
693
+ ### Checklist
694
+
695
+ - [ ] Component metadata has correct `name` and `node.key`
696
+ - [ ] All params have unique keys (5-6 char alphanumeric)
697
+ - [ ] Root Container has `isDummyRoot: true` and transparent background
698
+ - [ ] Tree outline uses `children` (not `body`) under the root node
699
+ - [ ] All file keys use `component/` prefix (not `page/`)
700
+ - [ ] Tree outline key is `component-widget-tree-outline` (not `page-widget-tree-outline`)
701
+ - [ ] Parameter references use `source: WIDGET_CLASS_PARAMETER` with correct `nodeKeyRef`
702
+ - [ ] Host Container has both `componentClassKeyRef.key` and `parameterValues.widgetClassNodeKeyRef.key` pointing to the same component root ID
703
+ - [ ] Action callbacks use `executeCallbackAction` with correct `parameterIdentifier`
704
+ - [ ] Validated all files before pushing
@@ -413,7 +413,28 @@ When passing YAML content to `validate_yaml` and `update_project_yaml`:
413
413
 
414
414
  ---
415
415
 
416
- ## 8. Recommended Workflow Patterns
416
+ ## 8. Creating and Refactoring Components
417
+
418
+ For full details on creating new components and refactoring page widgets into reusable components, see **[03-components.md](03-components.md)** sections 10-11.
419
+
420
+ ### Quick reference
421
+
422
+ **Creating a new component** requires pushing in a single call:
423
+ 1. Component metadata (`component/id-Container_XXX`) — name, params, state
424
+ 2. Widget tree outline (`component/id-Container_XXX/component-widget-tree-outline`) — uses `children` not `body`
425
+ 3. Root Container node with `isDummyRoot: true` — transparent background
426
+ 4. Individual child node files — same structure as page nodes
427
+
428
+ **Refactoring page widgets into a component:**
429
+ 1. Identify the subtree to extract
430
+ 2. Create component files (metadata + tree outline + root node + child nodes)
431
+ 3. Replace original page nodes with a single Container referencing the component via `componentClassKeyRef`
432
+ 4. Pass data through `parameterValues.parameterPasses`
433
+ 5. Push all files (component + updated page) in one `update_project_yaml` call
434
+
435
+ ---
436
+
437
+ ## 9. Recommended Workflow Patterns
417
438
 
418
439
  ### Inspecting a project for the first time
419
440
 
@@ -445,6 +466,28 @@ list_pages(projectId)
445
466
  --> update_project_yaml(projectId, { treeOutlineKey: ..., nodeKey1: ..., nodeKey2: ... })
446
467
  ```
447
468
 
469
+ ### Creating a reusable component
470
+
471
+ ```
472
+ [design component params and widget tree]
473
+ --> [construct metadata + tree outline + root node + child nodes]
474
+ --> validate_yaml for each file
475
+ --> update_project_yaml(projectId, { metadataKey: ..., treeKey: ..., rootNode: ..., childNodes: ... })
476
+ ```
477
+
478
+ See [03-components.md](03-components.md) sections 10-11 for complete walkthrough.
479
+
480
+ ### Refactoring page widgets into a component
481
+
482
+ ```
483
+ get_page_by_name(projectId, "PageName")
484
+ --> [identify subtree to extract, note all widget keys]
485
+ --> [create component files: metadata, tree outline, root node, child nodes]
486
+ --> [replace page subtree with componentClassKeyRef Container]
487
+ --> validate_yaml for all files
488
+ --> update_project_yaml(projectId, { ...componentFiles, ...updatedPageFiles })
489
+ ```
490
+
448
491
  ### Understanding navigation flow
449
492
 
450
493
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flutterflow-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
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",
@@ -32,6 +32,18 @@ list_pages → get_page_by_name → (node-level fetch) → validate_yaml → upd
32
32
  list_pages → get_page_by_name → update widget-tree-outline + push individual node files → validate_yaml → update_project_yaml
33
33
  ```
34
34
 
35
+ ### Creating a Reusable Component
36
+ ```
37
+ Design params → construct metadata + tree outline + root node (isDummyRoot) + child nodes → validate_yaml → update_project_yaml
38
+ ```
39
+ See `get_yaml_docs(topic: "create component")` for full walkthrough with examples.
40
+
41
+ ### Refactoring Page Widgets into a Component
42
+ ```
43
+ get_page_by_name → identify subtree → create component files → replace page subtree with componentClassKeyRef → push all files together
44
+ ```
45
+ See `get_yaml_docs(topic: "refactor")` for step-by-step guide.
46
+
35
47
  ### Finding Usages
36
48
  ```
37
49
  find_component_usages(componentName: "MyComponent") — where is a component used?