flutterflow-mcp 0.2.3 → 0.3.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.
- package/build/index.js +24 -0
- package/build/prompts/generate-page.js +6 -5
- package/build/prompts/modify-component.js +6 -5
- package/build/tools/find-component-usages.js +1 -10
- package/build/tools/find-page-navigations.js +1 -9
- package/build/tools/get-api-endpoints.d.ts +2 -0
- package/build/tools/get-api-endpoints.js +126 -0
- package/build/tools/get-app-settings.d.ts +2 -0
- package/build/tools/get-app-settings.js +169 -0
- package/build/tools/get-app-state.d.ts +2 -0
- package/build/tools/get-app-state.js +96 -0
- package/build/tools/get-custom-code.d.ts +2 -0
- package/build/tools/get-custom-code.js +380 -0
- package/build/tools/get-data-models.d.ts +2 -0
- package/build/tools/get-data-models.js +266 -0
- package/build/tools/get-editing-guide.d.ts +7 -0
- package/build/tools/get-editing-guide.js +185 -0
- package/build/tools/get-general-settings.d.ts +2 -0
- package/build/tools/get-general-settings.js +116 -0
- package/build/tools/get-in-app-purchases.d.ts +2 -0
- package/build/tools/get-in-app-purchases.js +51 -0
- package/build/tools/get-integrations.d.ts +2 -0
- package/build/tools/get-integrations.js +137 -0
- package/build/tools/get-page-summary.js +1 -18
- package/build/tools/get-project-setup.d.ts +2 -0
- package/build/tools/get-project-setup.js +212 -0
- package/build/tools/get-theme.d.ts +2 -0
- package/build/tools/get-theme.js +199 -0
- package/build/tools/get-yaml-docs.js +1 -111
- package/build/tools/list-files.js +22 -3
- package/build/tools/search-project-files.d.ts +2 -0
- package/build/tools/search-project-files.js +69 -0
- package/build/tools/sync-project.js +4 -1
- package/build/tools/update-yaml.js +1 -1
- package/build/tools/validate-yaml.js +19 -2
- package/build/utils/batch-process.d.ts +2 -0
- package/build/utils/batch-process.js +10 -0
- package/build/utils/cache.d.ts +5 -0
- package/build/utils/cache.js +16 -1
- package/build/utils/resolve-data-type.d.ts +2 -0
- package/build/utils/resolve-data-type.js +18 -0
- package/build/utils/topic-map.d.ts +7 -0
- package/build/utils/topic-map.js +115 -0
- package/docs/ff-yaml/00-overview.md +30 -1
- package/docs/ff-yaml/01-project-files.md +1380 -4
- package/docs/ff-yaml/08-custom-code.md +77 -1
- package/docs/ff-yaml/09-theming.md +46 -0
- package/package.json +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TOPIC_MAP, readDoc } from "../utils/topic-map.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Keyword sets for edit-type detection
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
const ADD_WIDGET_KEYWORDS = ["add", "new", "insert", "append", "place", "put"];
|
|
7
|
+
const COMPONENT_KEYWORDS = [
|
|
8
|
+
"component",
|
|
9
|
+
"reusable",
|
|
10
|
+
"refactor",
|
|
11
|
+
"extract",
|
|
12
|
+
];
|
|
13
|
+
const EDIT_KEYWORDS = [
|
|
14
|
+
"change",
|
|
15
|
+
"modify",
|
|
16
|
+
"update",
|
|
17
|
+
"edit",
|
|
18
|
+
"fix",
|
|
19
|
+
"set",
|
|
20
|
+
"remove",
|
|
21
|
+
"delete",
|
|
22
|
+
"hide",
|
|
23
|
+
"show",
|
|
24
|
+
"toggle",
|
|
25
|
+
"enable",
|
|
26
|
+
"disable",
|
|
27
|
+
"move",
|
|
28
|
+
"replace",
|
|
29
|
+
"rename",
|
|
30
|
+
"resize",
|
|
31
|
+
"restyle",
|
|
32
|
+
];
|
|
33
|
+
const CONFIG_KEYWORDS = [
|
|
34
|
+
"configure",
|
|
35
|
+
"settings",
|
|
36
|
+
"config",
|
|
37
|
+
"theme",
|
|
38
|
+
"auth",
|
|
39
|
+
"permission",
|
|
40
|
+
"navigation",
|
|
41
|
+
"nav",
|
|
42
|
+
"environment",
|
|
43
|
+
];
|
|
44
|
+
function detectEditType(words) {
|
|
45
|
+
const hasComponent = words.some((w) => COMPONENT_KEYWORDS.includes(w));
|
|
46
|
+
const hasAdd = words.some((w) => ADD_WIDGET_KEYWORDS.includes(w));
|
|
47
|
+
const hasConfig = words.some((w) => CONFIG_KEYWORDS.includes(w));
|
|
48
|
+
// Priority 1: component + (add/create/refactor)
|
|
49
|
+
if (hasComponent &&
|
|
50
|
+
(hasAdd || words.includes("create") || words.includes("refactor"))) {
|
|
51
|
+
return "create-component";
|
|
52
|
+
}
|
|
53
|
+
// Priority 2: config keywords
|
|
54
|
+
if (hasConfig) {
|
|
55
|
+
return "configure-project";
|
|
56
|
+
}
|
|
57
|
+
// Priority 3: add widget keywords
|
|
58
|
+
if (hasAdd) {
|
|
59
|
+
return "add-widget";
|
|
60
|
+
}
|
|
61
|
+
// Default
|
|
62
|
+
return "edit-existing";
|
|
63
|
+
}
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Workflow templates
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
function getWorkflow(editType, projectId) {
|
|
68
|
+
const pid = projectId ?? "projectId";
|
|
69
|
+
switch (editType) {
|
|
70
|
+
case "edit-existing":
|
|
71
|
+
return `## Workflow: Editing Existing Widgets
|
|
72
|
+
|
|
73
|
+
1. \`list_pages(${pid})\` — find the page
|
|
74
|
+
2. \`get_page_by_name(${pid}, "PageName")\` — read the page, find the widget key
|
|
75
|
+
3. \`get_project_yaml(${pid}, "page/id-Scaffold_XXX/.../node/id-Widget_YYY")\` — fetch node-level YAML
|
|
76
|
+
4. Modify the YAML (keep both \`inputValue\` and \`mostRecentInputValue\` in sync)
|
|
77
|
+
5. \`validate_yaml(${pid}, fileKey, yaml)\` — validate before pushing
|
|
78
|
+
6. \`update_project_yaml(${pid}, { fileKey: yaml })\` — push changes`;
|
|
79
|
+
case "add-widget":
|
|
80
|
+
return `## Workflow: Adding New Widgets
|
|
81
|
+
|
|
82
|
+
1. \`list_pages(${pid})\` — find the page
|
|
83
|
+
2. \`get_page_by_name(${pid}, "PageName")\` — read the page structure
|
|
84
|
+
3. Construct three types of files:
|
|
85
|
+
- Widget tree outline (\`page/id-Scaffold_XXX/page-widget-tree-outline\`)
|
|
86
|
+
- Parent node file (Column, Row, etc.)
|
|
87
|
+
- Individual child node files for each new widget
|
|
88
|
+
4. \`validate_yaml\` for each file
|
|
89
|
+
5. \`update_project_yaml(${pid}, { ...allFiles })\` — push ALL files in one call`;
|
|
90
|
+
case "create-component":
|
|
91
|
+
return `## Workflow: Creating/Refactoring Components
|
|
92
|
+
|
|
93
|
+
1. Read the existing page/component with \`get_page_by_name\` or \`get_component_summary\`
|
|
94
|
+
2. Construct component files:
|
|
95
|
+
- Component metadata (\`component/id-Container_XXX\`)
|
|
96
|
+
- Widget tree outline (\`component/id-Container_XXX/component-widget-tree-outline\`)
|
|
97
|
+
- Root Container node with \`isDummyRoot: true\`
|
|
98
|
+
- Individual child node files
|
|
99
|
+
3. If refactoring: update the source page to reference the component via \`componentClassKeyRef\`
|
|
100
|
+
4. \`validate_yaml\` for each file
|
|
101
|
+
5. \`update_project_yaml(${pid}, { ...allFiles })\` — push ALL files in one call
|
|
102
|
+
6. See \`get_yaml_docs(topic: "component")\` for full schema details`;
|
|
103
|
+
case "configure-project":
|
|
104
|
+
return `## Workflow: Configuring Project Settings
|
|
105
|
+
|
|
106
|
+
1. \`sync_project(${pid})\` — sync the project cache
|
|
107
|
+
2. Use cache tools to read current config:
|
|
108
|
+
- \`get_project_config\` — app details, auth, nav bar, permissions
|
|
109
|
+
- \`get_theme\` — colors, typography, breakpoints
|
|
110
|
+
- \`get_app_state\` — state variables, constants, environment
|
|
111
|
+
3. \`get_project_yaml(${pid}, "fileName")\` — fetch the specific config file
|
|
112
|
+
4. Modify the YAML
|
|
113
|
+
5. \`validate_yaml\` then \`update_project_yaml\``;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Universal rules
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
const UNIVERSAL_RULES = `## Critical YAML Rules
|
|
120
|
+
|
|
121
|
+
- **Always update both \`inputValue\` AND \`mostRecentInputValue\`** to the same value — they must stay in sync.
|
|
122
|
+
- **Exceptions:** \`fontWeightValue\` and \`fontSizeValue\` only accept \`inputValue\` (no \`mostRecentInputValue\`).
|
|
123
|
+
- **Use node-level file keys** for targeted edits, not the full page YAML.
|
|
124
|
+
- **Always validate before pushing** — call \`validate_yaml\` before \`update_project_yaml\`.
|
|
125
|
+
- **Adding widgets requires node-level files** — push the tree outline + individual nodes together.
|
|
126
|
+
- **Do NOT guess YAML field names** — use \`get_yaml_docs(topic: "...")\` to look them up.`;
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Doc resolution
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
function resolveDocFiles(words) {
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
const files = [];
|
|
133
|
+
for (const word of words) {
|
|
134
|
+
// Normalize: lowercase, strip dashes/underscores
|
|
135
|
+
const key = word.toLowerCase().replace(/[\s_-]+/g, "");
|
|
136
|
+
const docFile = TOPIC_MAP[key];
|
|
137
|
+
if (docFile && !seen.has(docFile)) {
|
|
138
|
+
seen.add(docFile);
|
|
139
|
+
files.push(docFile);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return files;
|
|
143
|
+
}
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Tool registration
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
export function registerGetEditingGuideTool(server) {
|
|
148
|
+
server.tool("get_editing_guide", "Get the recommended workflow and relevant documentation for a FlutterFlow editing task. Call this BEFORE modifying any YAML. Describe what you want to do (e.g. 'change button color', 'add a TextField to the login page', 'create a reusable header component') and receive the correct workflow steps, YAML schemas, and critical rules.", {
|
|
149
|
+
task: z
|
|
150
|
+
.string()
|
|
151
|
+
.describe("Natural language description of what you want to do"),
|
|
152
|
+
projectId: z
|
|
153
|
+
.string()
|
|
154
|
+
.optional()
|
|
155
|
+
.describe("Optional project ID to include in workflow steps"),
|
|
156
|
+
}, async ({ task, projectId }) => {
|
|
157
|
+
// Tokenize into lowercase words
|
|
158
|
+
const words = task.toLowerCase().split(/\W+/).filter(Boolean);
|
|
159
|
+
// Detect edit type
|
|
160
|
+
const editType = detectEditType(words);
|
|
161
|
+
// Build workflow section
|
|
162
|
+
const workflow = getWorkflow(editType, projectId);
|
|
163
|
+
// Resolve matching doc files
|
|
164
|
+
const docFiles = resolveDocFiles(words);
|
|
165
|
+
// Read matched docs
|
|
166
|
+
const docSections = [];
|
|
167
|
+
for (const file of docFiles) {
|
|
168
|
+
const content = readDoc(file);
|
|
169
|
+
if (content) {
|
|
170
|
+
docSections.push(`---\n\n# Reference: ${file}\n\n${content}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Assemble response
|
|
174
|
+
const parts = [workflow, "", UNIVERSAL_RULES, ""];
|
|
175
|
+
if (docSections.length > 0) {
|
|
176
|
+
parts.push(...docSections);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
parts.push('No specific widget/topic docs matched your task. Use `get_yaml_docs(topic: "...")` to search for relevant schemas, or `get_yaml_docs()` for the full index.');
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
import { cacheRead, cacheMeta } from "../utils/cache.js";
|
|
4
|
+
async function resolvePageName(projectId, scaffoldId) {
|
|
5
|
+
const content = await cacheRead(projectId, `page/id-${scaffoldId}`);
|
|
6
|
+
if (!content)
|
|
7
|
+
return scaffoldId;
|
|
8
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
9
|
+
return nameMatch ? nameMatch[1].trim() : scaffoldId;
|
|
10
|
+
}
|
|
11
|
+
export function registerGetGeneralSettingsTool(server) {
|
|
12
|
+
server.tool("get_general_settings", "Get General settings — App Details (name, package, initial page, routing), App Assets (icon, splash, error image), Nav Bar & App Bar. Mirrors the FlutterFlow 'General' settings section. Cache-based, no API calls. Run sync_project first.", {
|
|
13
|
+
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
14
|
+
}, async ({ projectId }) => {
|
|
15
|
+
const meta = await cacheMeta(projectId);
|
|
16
|
+
if (!meta) {
|
|
17
|
+
return {
|
|
18
|
+
content: [
|
|
19
|
+
{
|
|
20
|
+
type: "text",
|
|
21
|
+
text: `No cache found for project "${projectId}". Run sync_project first.`,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const [appDetailsRaw, appAssetsRaw, navBarRaw] = await Promise.all([
|
|
27
|
+
cacheRead(projectId, "app-details"),
|
|
28
|
+
cacheRead(projectId, "app-assets"),
|
|
29
|
+
cacheRead(projectId, "nav-bar"),
|
|
30
|
+
]);
|
|
31
|
+
const sections = ["# General Settings"];
|
|
32
|
+
// --- App Details ---
|
|
33
|
+
if (appDetailsRaw) {
|
|
34
|
+
const appDetails = YAML.parse(appDetailsRaw);
|
|
35
|
+
const appName = appDetails.name || "Unknown";
|
|
36
|
+
// Package name: take first env's packageName
|
|
37
|
+
let packageName = "not set";
|
|
38
|
+
const allAppNames = appDetails.allAppNames;
|
|
39
|
+
const appNames = allAppNames?.appNames;
|
|
40
|
+
if (appNames) {
|
|
41
|
+
const firstEnvKey = Object.keys(appNames)[0];
|
|
42
|
+
if (firstEnvKey) {
|
|
43
|
+
packageName = appNames[firstEnvKey].packageName || "not set";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Initial page
|
|
47
|
+
const initialPageKey = appDetails.initialPageKeyRef?.key;
|
|
48
|
+
let initialPageLine = "not set";
|
|
49
|
+
if (initialPageKey) {
|
|
50
|
+
const name = await resolvePageName(projectId, initialPageKey);
|
|
51
|
+
initialPageLine = `${name} (${initialPageKey})`;
|
|
52
|
+
}
|
|
53
|
+
// Routing
|
|
54
|
+
const routing = appDetails.routingSettings;
|
|
55
|
+
const routingEnabled = routing?.enableRouting === true;
|
|
56
|
+
const subroutes = routing?.pagesAreSubroutesOfRoot === true;
|
|
57
|
+
sections.push(`\n## App Details`, `Name: ${appName}`, `Package name: ${packageName}`, `Initial page: ${initialPageLine}`, `Routing: ${routingEnabled ? "enabled" : "disabled"}, pages are subroutes: ${subroutes ? "yes" : "no"}`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
sections.push(`\n## App Details`, `(not configured)`);
|
|
61
|
+
}
|
|
62
|
+
// --- App Assets ---
|
|
63
|
+
if (appAssetsRaw) {
|
|
64
|
+
const appAssets = YAML.parse(appAssetsRaw);
|
|
65
|
+
const appIconPath = appAssets.appIconPath || "not set";
|
|
66
|
+
const splashImage = appAssets.splashImage;
|
|
67
|
+
const errorImagePath = appAssets.errorImagePath || "not set";
|
|
68
|
+
sections.push(`\n## App Assets`);
|
|
69
|
+
sections.push(`App icon: ${appIconPath}`);
|
|
70
|
+
if (splashImage) {
|
|
71
|
+
const splashPath = splashImage.path || "not set";
|
|
72
|
+
const splashFit = splashImage.fit || "not set";
|
|
73
|
+
const splashDuration = splashImage.minSplashScreenDuration;
|
|
74
|
+
sections.push(`Splash image: ${splashPath} (fit: ${splashFit})`);
|
|
75
|
+
if (splashDuration)
|
|
76
|
+
sections.push(`Splash duration: ${splashDuration}ms`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
sections.push(`Splash image: not set`);
|
|
80
|
+
}
|
|
81
|
+
sections.push(`Error image: ${errorImagePath}`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
sections.push(`\n## App Assets`, `(not configured)`);
|
|
85
|
+
}
|
|
86
|
+
// --- Nav Bar & App Bar ---
|
|
87
|
+
if (navBarRaw) {
|
|
88
|
+
const navBar = YAML.parse(navBarRaw);
|
|
89
|
+
const visible = navBar.show === true;
|
|
90
|
+
if (visible) {
|
|
91
|
+
const navType = navBar.navBarType || "UNKNOWN";
|
|
92
|
+
const labels = navBar.labels === true;
|
|
93
|
+
const pageRefs = navBar.pageKeyRefOrder;
|
|
94
|
+
sections.push(`\n## Nav Bar & App Bar`, `Visible: Yes`, `Type: ${navType}`, `Labels: ${labels ? "Yes" : "No"}`);
|
|
95
|
+
if (pageRefs && pageRefs.length > 0) {
|
|
96
|
+
const tabs = [];
|
|
97
|
+
for (let i = 0; i < pageRefs.length; i++) {
|
|
98
|
+
const scaffoldId = pageRefs[i].key;
|
|
99
|
+
const name = await resolvePageName(projectId, scaffoldId);
|
|
100
|
+
tabs.push(` ${i + 1}. ${name} (${scaffoldId})`);
|
|
101
|
+
}
|
|
102
|
+
sections.push(`Tabs:`, ...tabs);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
sections.push(`\n## Nav Bar & App Bar`, `Visible: No`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
sections.push(`\n## Nav Bar & App Bar`, `(not configured)`);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text: sections.join("\n") }],
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
import { cacheRead, cacheMeta } from "../utils/cache.js";
|
|
4
|
+
const PROVIDERS = [
|
|
5
|
+
{ key: "stripe", label: "Stripe" },
|
|
6
|
+
{ key: "braintree", label: "Braintree" },
|
|
7
|
+
{ key: "revenue-cat", label: "RevenueCat" },
|
|
8
|
+
{ key: "razorpay", label: "Razorpay" },
|
|
9
|
+
];
|
|
10
|
+
export function registerGetInAppPurchasesTool(server) {
|
|
11
|
+
server.tool("get_in_app_purchases", "Get In-App Purchases & Subscriptions settings — Stripe, Braintree, RevenueCat, Razorpay. Mirrors the FlutterFlow 'In App Purchases & Subscriptions' section. Cache-based, no API calls. Run sync_project first.", {
|
|
12
|
+
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
13
|
+
}, async ({ projectId }) => {
|
|
14
|
+
const meta = await cacheMeta(projectId);
|
|
15
|
+
if (!meta) {
|
|
16
|
+
return {
|
|
17
|
+
content: [
|
|
18
|
+
{
|
|
19
|
+
type: "text",
|
|
20
|
+
text: `No cache found for project "${projectId}". Run sync_project first.`,
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const sections = ["# In-App Purchases & Subscriptions"];
|
|
26
|
+
for (const provider of PROVIDERS) {
|
|
27
|
+
const raw = await cacheRead(projectId, provider.key);
|
|
28
|
+
sections.push(`\n## ${provider.label}`);
|
|
29
|
+
if (!raw) {
|
|
30
|
+
sections.push("(not configured)");
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const doc = YAML.parse(raw);
|
|
34
|
+
const enabled = doc.enabled === true;
|
|
35
|
+
sections.push(`Enabled: ${enabled ? "Yes" : "No"}`);
|
|
36
|
+
// Show any additional top-level scalar fields (skip 'enabled' itself and complex objects)
|
|
37
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
38
|
+
if (key === "enabled")
|
|
39
|
+
continue;
|
|
40
|
+
if (value === null || value === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
if (typeof value === "object")
|
|
43
|
+
continue;
|
|
44
|
+
sections.push(`${key}: ${String(value)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text", text: sections.join("\n") }],
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
import { cacheRead, cacheMeta } from "../utils/cache.js";
|
|
4
|
+
/**
|
|
5
|
+
* Fields that should never be output — API keys, secrets, tokens, etc.
|
|
6
|
+
* Only IDs and non-sensitive configuration are shown.
|
|
7
|
+
*/
|
|
8
|
+
const SENSITIVE_KEYS = new Set([
|
|
9
|
+
"searchApiKey",
|
|
10
|
+
"supabaseAnonKey",
|
|
11
|
+
"supabaseServiceRoleKey",
|
|
12
|
+
"apiKey",
|
|
13
|
+
"secretKey",
|
|
14
|
+
"secret",
|
|
15
|
+
"token",
|
|
16
|
+
"accessToken",
|
|
17
|
+
"refreshToken",
|
|
18
|
+
"privateKey",
|
|
19
|
+
]);
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Custom formatters for specific integrations
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
function formatAlgolia(doc) {
|
|
24
|
+
const lines = [];
|
|
25
|
+
const enabled = doc.enabled === true;
|
|
26
|
+
lines.push(`Enabled: ${enabled ? "Yes" : "No"}`);
|
|
27
|
+
const appId = doc.applicationId || "not set";
|
|
28
|
+
lines.push(`Application ID: ${appId}`);
|
|
29
|
+
const collections = doc.indexedCollections;
|
|
30
|
+
if (collections && collections.length > 0) {
|
|
31
|
+
lines.push(`Indexed collections:`);
|
|
32
|
+
for (const col of collections) {
|
|
33
|
+
lines.push(` - ${col.name || "unnamed"}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
lines.push(`Indexed collections: none`);
|
|
38
|
+
}
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
function formatGoogleMaps(doc) {
|
|
42
|
+
const androidKey = doc.androidKey || "not set";
|
|
43
|
+
const iosKey = doc.iosKey || "not set";
|
|
44
|
+
const webKey = doc.webKey || "not set";
|
|
45
|
+
return [
|
|
46
|
+
`Android key: ${androidKey}`,
|
|
47
|
+
`iOS key: ${iosKey}`,
|
|
48
|
+
`Web key: ${webKey}`,
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
function formatFirebaseAnalytics(doc) {
|
|
52
|
+
const lines = [];
|
|
53
|
+
const enabled = doc.enabled === true;
|
|
54
|
+
lines.push(`Enabled: ${enabled ? "Yes" : "No"}`);
|
|
55
|
+
const settings = doc.automaticEventSettings;
|
|
56
|
+
if (settings) {
|
|
57
|
+
lines.push(`Automatic event settings:`);
|
|
58
|
+
lines.push(` onPageLoad: ${settings.onPageLoad === true ? "Yes" : "No"}`);
|
|
59
|
+
lines.push(` onActionsStart: ${settings.onActionsStart === true ? "Yes" : "No"}`);
|
|
60
|
+
lines.push(` onAuth: ${settings.onAuth === true ? "Yes" : "No"}`);
|
|
61
|
+
}
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Generic formatter: shows `enabled` status and all non-sensitive
|
|
66
|
+
* top-level string/boolean fields.
|
|
67
|
+
*/
|
|
68
|
+
function formatGeneric(doc) {
|
|
69
|
+
const lines = [];
|
|
70
|
+
if ("enabled" in doc) {
|
|
71
|
+
lines.push(`Enabled: ${doc.enabled === true ? "Yes" : "No"}`);
|
|
72
|
+
}
|
|
73
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
74
|
+
if (key === "enabled")
|
|
75
|
+
continue;
|
|
76
|
+
if (SENSITIVE_KEYS.has(key))
|
|
77
|
+
continue;
|
|
78
|
+
if (typeof value === "string" || typeof value === "boolean") {
|
|
79
|
+
lines.push(`${key}: ${typeof value === "boolean" ? (value ? "Yes" : "No") : value}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return lines;
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Integration list — order matches the spec
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
const INTEGRATIONS = [
|
|
88
|
+
{ fileKey: "supabase", displayName: "Supabase" },
|
|
89
|
+
{ fileKey: "sqlite", displayName: "SQLite" },
|
|
90
|
+
{ fileKey: "github", displayName: "GitHub" },
|
|
91
|
+
{ fileKey: "algolia", displayName: "Algolia", format: formatAlgolia },
|
|
92
|
+
{ fileKey: "firebase-analytics", displayName: "Google Analytics", format: formatFirebaseAnalytics },
|
|
93
|
+
{ fileKey: "google-maps", displayName: "Google Maps", format: formatGoogleMaps },
|
|
94
|
+
{ fileKey: "admob", displayName: "AdMob" },
|
|
95
|
+
{ fileKey: "mux", displayName: "Mux Livestream" },
|
|
96
|
+
{ fileKey: "onesignal", displayName: "OneSignal" },
|
|
97
|
+
{ fileKey: "gemini", displayName: "Gemini" },
|
|
98
|
+
];
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Tool registration
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
export function registerGetIntegrationsTool(server) {
|
|
103
|
+
server.tool("get_integrations", "Get Integrations settings — Supabase, SQLite, GitHub, Algolia, Google Analytics, Google Maps, AdMob, Mux Livestream, OneSignal, Gemini. Mirrors the FlutterFlow 'Integrations' section. Cache-based, no API calls. Run sync_project first.", {
|
|
104
|
+
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
105
|
+
}, async ({ projectId }) => {
|
|
106
|
+
const meta = await cacheMeta(projectId);
|
|
107
|
+
if (!meta) {
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: `No cache found for project "${projectId}". Run sync_project first.`,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// Read all integration files in parallel
|
|
118
|
+
const rawResults = await Promise.all(INTEGRATIONS.map((def) => cacheRead(projectId, def.fileKey)));
|
|
119
|
+
const sections = ["# Integrations"];
|
|
120
|
+
for (let i = 0; i < INTEGRATIONS.length; i++) {
|
|
121
|
+
const def = INTEGRATIONS[i];
|
|
122
|
+
const raw = rawResults[i];
|
|
123
|
+
sections.push(`\n## ${def.displayName}`);
|
|
124
|
+
if (!raw) {
|
|
125
|
+
sections.push("(not configured)");
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const doc = YAML.parse(raw);
|
|
129
|
+
const formatter = def.format || formatGeneric;
|
|
130
|
+
const lines = formatter(doc);
|
|
131
|
+
sections.push(lines.join("\n"));
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text: sections.join("\n") }],
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -6,6 +6,7 @@ import { parseTreeOutline } from "../utils/page-summary/tree-walker.js";
|
|
|
6
6
|
import { extractNodeInfo } from "../utils/page-summary/node-extractor.js";
|
|
7
7
|
import { summarizeTriggers } from "../utils/page-summary/action-summarizer.js";
|
|
8
8
|
import { formatPageSummary } from "../utils/page-summary/formatter.js";
|
|
9
|
+
import { resolveDataType } from "../utils/resolve-data-type.js";
|
|
9
10
|
/**
|
|
10
11
|
* Resolve a page name or scaffold ID to its cache file key.
|
|
11
12
|
* Returns all available page names if no match found.
|
|
@@ -41,24 +42,6 @@ export async function resolvePage(projectId, pageName, scaffoldId) {
|
|
|
41
42
|
// ---------------------------------------------------------------------------
|
|
42
43
|
// Metadata extraction
|
|
43
44
|
// ---------------------------------------------------------------------------
|
|
44
|
-
/** Resolve a scalar/list data type to a readable string. */
|
|
45
|
-
function resolveDataType(dt) {
|
|
46
|
-
if (dt.listType) {
|
|
47
|
-
const inner = dt.listType;
|
|
48
|
-
return `List<${inner.scalarType || "unknown"}>`;
|
|
49
|
-
}
|
|
50
|
-
if (dt.scalarType === "DataStruct") {
|
|
51
|
-
const sub = dt.subType;
|
|
52
|
-
const dsi = sub?.dataStructIdentifier;
|
|
53
|
-
return dsi?.name ? `DataStruct:${dsi.name}` : "DataStruct";
|
|
54
|
-
}
|
|
55
|
-
if (dt.enumType) {
|
|
56
|
-
const en = dt.enumType;
|
|
57
|
-
const eid = en.enumIdentifier;
|
|
58
|
-
return eid?.name ? `Enum:${eid.name}` : "Enum";
|
|
59
|
-
}
|
|
60
|
-
return dt.scalarType || "unknown";
|
|
61
|
-
}
|
|
62
45
|
/** Extract page metadata (name, params, state) from the top-level page YAML. */
|
|
63
46
|
async function extractPageMeta(projectId, page, folder) {
|
|
64
47
|
const content = await cacheRead(projectId, page.pageFileKey);
|