community-ff-mcp 0.4.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/README.md +300 -0
- package/build/api/flutterflow.d.ts +11 -0
- package/build/api/flutterflow.js +61 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +78 -0
- package/build/prompts/dev-workflow.d.ts +2 -0
- package/build/prompts/dev-workflow.js +68 -0
- package/build/prompts/generate-page.d.ts +2 -0
- package/build/prompts/generate-page.js +37 -0
- package/build/prompts/inspect-project.d.ts +2 -0
- package/build/prompts/inspect-project.js +30 -0
- package/build/prompts/modify-component.d.ts +2 -0
- package/build/prompts/modify-component.js +40 -0
- package/build/resources/docs.d.ts +2 -0
- package/build/resources/docs.js +76 -0
- package/build/resources/projects.d.ts +3 -0
- package/build/resources/projects.js +60 -0
- package/build/tools/find-component-usages.d.ts +21 -0
- package/build/tools/find-component-usages.js +216 -0
- package/build/tools/find-page-navigations.d.ts +26 -0
- package/build/tools/find-page-navigations.js +220 -0
- 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-component-summary.d.ts +22 -0
- package/build/tools/get-component-summary.js +195 -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-by-name.d.ts +3 -0
- package/build/tools/get-page-by-name.js +56 -0
- package/build/tools/get-page-summary.d.ts +22 -0
- package/build/tools/get-page-summary.js +205 -0
- package/build/tools/get-project-config.d.ts +2 -0
- package/build/tools/get-project-config.js +216 -0
- 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.d.ts +6 -0
- package/build/tools/get-yaml-docs.js +116 -0
- package/build/tools/get-yaml.d.ts +2 -0
- package/build/tools/get-yaml.js +53 -0
- package/build/tools/list-files.d.ts +3 -0
- package/build/tools/list-files.js +49 -0
- package/build/tools/list-pages.d.ts +25 -0
- package/build/tools/list-pages.js +101 -0
- package/build/tools/list-projects.d.ts +3 -0
- package/build/tools/list-projects.js +23 -0
- package/build/tools/search-project-files.d.ts +2 -0
- package/build/tools/search-project-files.js +69 -0
- package/build/tools/sync-project.d.ts +3 -0
- package/build/tools/sync-project.js +147 -0
- package/build/tools/update-yaml.d.ts +3 -0
- package/build/tools/update-yaml.js +24 -0
- package/build/tools/validate-yaml.d.ts +3 -0
- package/build/tools/validate-yaml.js +39 -0
- package/build/utils/batch-process.d.ts +2 -0
- package/build/utils/batch-process.js +10 -0
- package/build/utils/cache.d.ts +58 -0
- package/build/utils/cache.js +199 -0
- package/build/utils/decode-yaml.d.ts +7 -0
- package/build/utils/decode-yaml.js +31 -0
- package/build/utils/page-summary/action-summarizer.d.ts +24 -0
- package/build/utils/page-summary/action-summarizer.js +291 -0
- package/build/utils/page-summary/formatter.d.ts +13 -0
- package/build/utils/page-summary/formatter.js +129 -0
- package/build/utils/page-summary/node-extractor.d.ts +24 -0
- package/build/utils/page-summary/node-extractor.js +227 -0
- package/build/utils/page-summary/tree-walker.d.ts +6 -0
- package/build/utils/page-summary/tree-walker.js +55 -0
- package/build/utils/page-summary/types.d.ts +58 -0
- package/build/utils/page-summary/types.js +4 -0
- package/build/utils/parse-folders.d.ts +9 -0
- package/build/utils/parse-folders.js +29 -0
- 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 +122 -0
- package/docs/ff-yaml/00-overview.md +166 -0
- package/docs/ff-yaml/01-project-files.md +2309 -0
- package/docs/ff-yaml/02-pages.md +572 -0
- package/docs/ff-yaml/03-components.md +784 -0
- package/docs/ff-yaml/04-widgets/README.md +122 -0
- package/docs/ff-yaml/04-widgets/button.md +444 -0
- package/docs/ff-yaml/04-widgets/container.md +358 -0
- package/docs/ff-yaml/04-widgets/dropdown.md +579 -0
- package/docs/ff-yaml/04-widgets/form.md +256 -0
- package/docs/ff-yaml/04-widgets/image.md +276 -0
- package/docs/ff-yaml/04-widgets/layout.md +355 -0
- package/docs/ff-yaml/04-widgets/misc.md +553 -0
- package/docs/ff-yaml/04-widgets/text-field.md +326 -0
- package/docs/ff-yaml/04-widgets/text.md +302 -0
- package/docs/ff-yaml/05-actions.md +953 -0
- package/docs/ff-yaml/06-variables.md +849 -0
- package/docs/ff-yaml/07-data.md +591 -0
- package/docs/ff-yaml/08-custom-code.md +736 -0
- package/docs/ff-yaml/09-theming.md +638 -0
- package/docs/ff-yaml/10-editing-guide.md +497 -0
- package/docs/ff-yaml/README.md +105 -0
- package/package.json +59 -0
- package/skills/community-ff-mcp/SKILL.md +201 -0
- package/skills/ff-widget-patterns.md +141 -0
- package/skills/ff-yaml-dev.md +70 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
import { cacheRead, cacheMeta, cacheAgeFooter, listCachedKeys, } from "../utils/cache.js";
|
|
4
|
+
import { parseFolderMapping } from "../utils/parse-folders.js";
|
|
5
|
+
import { parseTreeOutline } from "../utils/page-summary/tree-walker.js";
|
|
6
|
+
import { extractNodeInfo } from "../utils/page-summary/node-extractor.js";
|
|
7
|
+
import { summarizeTriggers } from "../utils/page-summary/action-summarizer.js";
|
|
8
|
+
import { formatPageSummary } from "../utils/page-summary/formatter.js";
|
|
9
|
+
import { resolveDataType } from "../utils/resolve-data-type.js";
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a page name or scaffold ID to its cache file key.
|
|
12
|
+
* Returns all available page names if no match found.
|
|
13
|
+
*/
|
|
14
|
+
export async function resolvePage(projectId, pageName, scaffoldId) {
|
|
15
|
+
if (scaffoldId) {
|
|
16
|
+
const pageFileKey = `page/id-${scaffoldId}`;
|
|
17
|
+
const content = await cacheRead(projectId, pageFileKey);
|
|
18
|
+
if (content) {
|
|
19
|
+
return { ok: true, page: { scaffoldId, pageFileKey } };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// List all cached page top-level files
|
|
23
|
+
const allKeys = await listCachedKeys(projectId, "page/id-Scaffold_");
|
|
24
|
+
// Filter to top-level page files only (not sub-files)
|
|
25
|
+
const pageKeys = allKeys.filter((k) => /^page\/id-Scaffold_\w+$/.test(k));
|
|
26
|
+
const available = [];
|
|
27
|
+
for (const key of pageKeys) {
|
|
28
|
+
const content = await cacheRead(projectId, key);
|
|
29
|
+
if (!content)
|
|
30
|
+
continue;
|
|
31
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
32
|
+
const name = nameMatch ? nameMatch[1].trim() : "";
|
|
33
|
+
if (pageName && name.toLowerCase() === pageName.toLowerCase()) {
|
|
34
|
+
const sid = key.match(/^page\/id-(Scaffold_\w+)$/)?.[1] || "";
|
|
35
|
+
return { ok: true, page: { scaffoldId: sid, pageFileKey: key } };
|
|
36
|
+
}
|
|
37
|
+
if (name)
|
|
38
|
+
available.push(name);
|
|
39
|
+
}
|
|
40
|
+
return { ok: false, available };
|
|
41
|
+
}
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Metadata extraction
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
/** Extract page metadata (name, params, state) from the top-level page YAML. */
|
|
46
|
+
async function extractPageMeta(projectId, page, folder) {
|
|
47
|
+
const content = await cacheRead(projectId, page.pageFileKey);
|
|
48
|
+
if (!content) {
|
|
49
|
+
return {
|
|
50
|
+
pageName: page.scaffoldId,
|
|
51
|
+
scaffoldId: page.scaffoldId,
|
|
52
|
+
folder,
|
|
53
|
+
params: [],
|
|
54
|
+
stateFields: [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const doc = YAML.parse(content);
|
|
58
|
+
const pageName = doc.name || page.scaffoldId;
|
|
59
|
+
// Params
|
|
60
|
+
const params = [];
|
|
61
|
+
const rawParams = doc.params;
|
|
62
|
+
if (rawParams) {
|
|
63
|
+
for (const val of Object.values(rawParams)) {
|
|
64
|
+
const id = val.identifier;
|
|
65
|
+
const name = id?.name || "unknown";
|
|
66
|
+
const dt = val.dataType || {};
|
|
67
|
+
const defaultVal = val.defaultValue;
|
|
68
|
+
params.push({
|
|
69
|
+
name,
|
|
70
|
+
dataType: resolveDataType(dt),
|
|
71
|
+
defaultValue: defaultVal?.serializedValue,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// State fields
|
|
76
|
+
const stateFields = [];
|
|
77
|
+
const classModel = doc.classModel;
|
|
78
|
+
const rawFields = classModel?.stateFields;
|
|
79
|
+
if (Array.isArray(rawFields)) {
|
|
80
|
+
for (const field of rawFields) {
|
|
81
|
+
const param = field.parameter;
|
|
82
|
+
if (!param)
|
|
83
|
+
continue;
|
|
84
|
+
const id = param.identifier;
|
|
85
|
+
const name = id?.name || "unknown";
|
|
86
|
+
const dt = param.dataType || {};
|
|
87
|
+
const defaultVals = field.serializedDefaultValue;
|
|
88
|
+
stateFields.push({
|
|
89
|
+
name,
|
|
90
|
+
dataType: resolveDataType(dt),
|
|
91
|
+
defaultValue: defaultVals?.[0],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { pageName, scaffoldId: page.scaffoldId, folder, params, stateFields };
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Tree enrichment
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
/**
|
|
101
|
+
* Recursively enrich an OutlineNode tree with node info and trigger summaries.
|
|
102
|
+
*/
|
|
103
|
+
async function enrichNode(projectId, pagePrefix, // e.g. "page/id-Scaffold_xxx/page-widget-tree-outline"
|
|
104
|
+
outline) {
|
|
105
|
+
const nodeInfo = await extractNodeInfo(projectId, pagePrefix, outline.key);
|
|
106
|
+
// Check for triggers
|
|
107
|
+
const nodeFilePrefix = `${pagePrefix}/node/id-${outline.key}`;
|
|
108
|
+
const triggers = await summarizeTriggers(projectId, nodeFilePrefix);
|
|
109
|
+
// Enrich children
|
|
110
|
+
const children = await Promise.all(outline.children.map((child) => enrichNode(projectId, pagePrefix, child)));
|
|
111
|
+
return {
|
|
112
|
+
key: outline.key,
|
|
113
|
+
type: nodeInfo.type,
|
|
114
|
+
name: nodeInfo.name,
|
|
115
|
+
slot: outline.slot,
|
|
116
|
+
detail: nodeInfo.detail,
|
|
117
|
+
componentRef: nodeInfo.componentRef,
|
|
118
|
+
componentId: nodeInfo.componentId,
|
|
119
|
+
triggers,
|
|
120
|
+
children,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Tool registration
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
export function registerGetPageSummaryTool(server) {
|
|
127
|
+
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.", {
|
|
128
|
+
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
129
|
+
pageName: z
|
|
130
|
+
.string()
|
|
131
|
+
.optional()
|
|
132
|
+
.describe("Human-readable page name (e.g. 'PaywallPage'). Case-insensitive. Provide either pageName or scaffoldId."),
|
|
133
|
+
scaffoldId: z
|
|
134
|
+
.string()
|
|
135
|
+
.optional()
|
|
136
|
+
.describe("Scaffold ID (e.g. 'Scaffold_tydsj8ql'). Provide either pageName or scaffoldId."),
|
|
137
|
+
}, async ({ projectId, pageName, scaffoldId }) => {
|
|
138
|
+
// Check cache exists
|
|
139
|
+
const meta = await cacheMeta(projectId);
|
|
140
|
+
if (!meta) {
|
|
141
|
+
return {
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: "text",
|
|
145
|
+
text: `No cache found for project "${projectId}". Run sync_project first to download the project YAML files.`,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (!pageName && !scaffoldId) {
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: "text",
|
|
155
|
+
text: "Provide either pageName or scaffoldId.",
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// Resolve page
|
|
161
|
+
const resolved = await resolvePage(projectId, pageName, scaffoldId);
|
|
162
|
+
if (!resolved.ok) {
|
|
163
|
+
const list = resolved.available.map((n) => ` - ${n}`).join("\n");
|
|
164
|
+
const searchTerm = pageName || scaffoldId || "";
|
|
165
|
+
return {
|
|
166
|
+
content: [
|
|
167
|
+
{
|
|
168
|
+
type: "text",
|
|
169
|
+
text: `Page "${searchTerm}" not found in cache. Available pages:\n${list}`,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const page = resolved.page;
|
|
175
|
+
// Get folder mapping
|
|
176
|
+
const foldersContent = await cacheRead(projectId, "folders");
|
|
177
|
+
const folderMap = foldersContent
|
|
178
|
+
? parseFolderMapping(foldersContent)
|
|
179
|
+
: {};
|
|
180
|
+
const folder = folderMap[page.scaffoldId] || "(unmapped)";
|
|
181
|
+
// Extract metadata
|
|
182
|
+
const pageMeta = await extractPageMeta(projectId, page, folder);
|
|
183
|
+
// Parse widget tree outline
|
|
184
|
+
const outlineKey = `${page.pageFileKey}/page-widget-tree-outline`;
|
|
185
|
+
const outlineContent = await cacheRead(projectId, outlineKey);
|
|
186
|
+
if (!outlineContent) {
|
|
187
|
+
return {
|
|
188
|
+
content: [
|
|
189
|
+
{
|
|
190
|
+
type: "text",
|
|
191
|
+
text: `Page "${pageMeta.pageName}" found but widget tree outline is not cached. Re-run sync_project to fetch all sub-files.`,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const outlineTree = parseTreeOutline(outlineContent);
|
|
197
|
+
// Enrich the tree with node info and triggers
|
|
198
|
+
const enrichedTree = await enrichNode(projectId, outlineKey, outlineTree);
|
|
199
|
+
// Format output
|
|
200
|
+
const summary = formatPageSummary(pageMeta, enrichedTree);
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: "text", text: summary + cacheAgeFooter(meta) }],
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
import { cacheRead, cacheMeta, listCachedKeys } 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 registerGetProjectConfigTool(server) {
|
|
12
|
+
server.tool("get_project_config", "Get core project configuration — app name, entry pages, routing, nav bar, auth, permissions, services, main.dart lifecycle actions, and a project file map. No API calls. Run sync_project first if not cached.", {
|
|
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 to download the project YAML files.`,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const [appDetailsRaw, navBarRaw, authRaw, permissionsRaw, revenueCatRaw] = await Promise.all([
|
|
27
|
+
cacheRead(projectId, "app-details"),
|
|
28
|
+
cacheRead(projectId, "nav-bar"),
|
|
29
|
+
cacheRead(projectId, "authentication"),
|
|
30
|
+
cacheRead(projectId, "permissions"),
|
|
31
|
+
cacheRead(projectId, "revenue-cat"),
|
|
32
|
+
]);
|
|
33
|
+
const sections = ["# Project Configuration"];
|
|
34
|
+
// --- App Details ---
|
|
35
|
+
if (appDetailsRaw) {
|
|
36
|
+
const appDetails = YAML.parse(appDetailsRaw);
|
|
37
|
+
const appName = appDetails.name || "Unknown";
|
|
38
|
+
const initialPageKey = appDetails.initialPageKeyRef?.key;
|
|
39
|
+
const routing = appDetails.routingSettings;
|
|
40
|
+
const routingEnabled = routing?.enableRouting === true;
|
|
41
|
+
const subroutes = routing?.pagesAreSubroutesOfRoot === true;
|
|
42
|
+
let initialPageLine = "not set";
|
|
43
|
+
if (initialPageKey) {
|
|
44
|
+
const name = await resolvePageName(projectId, initialPageKey);
|
|
45
|
+
initialPageLine = `${name} (${initialPageKey})`;
|
|
46
|
+
}
|
|
47
|
+
sections.push(`\n## App Details`, `Name: ${appName}`, `Initial page: ${initialPageLine}`, `Routing: ${routingEnabled ? "enabled" : "disabled"}, pages are subroutes: ${subroutes ? "yes" : "no"}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
sections.push(`\n## App Details`, `(not cached)`);
|
|
51
|
+
}
|
|
52
|
+
// --- Authentication ---
|
|
53
|
+
if (authRaw) {
|
|
54
|
+
const auth = YAML.parse(authRaw);
|
|
55
|
+
const active = auth.active === true;
|
|
56
|
+
if (active) {
|
|
57
|
+
let provider = "Unknown";
|
|
58
|
+
const firebaseConfigs = auth.firebaseConfigFileInfos;
|
|
59
|
+
const supabase = auth.supabase;
|
|
60
|
+
if (firebaseConfigs && firebaseConfigs.length > 0) {
|
|
61
|
+
provider = "Firebase";
|
|
62
|
+
}
|
|
63
|
+
else if (supabase && Object.keys(supabase).length > 0) {
|
|
64
|
+
provider = "Supabase";
|
|
65
|
+
}
|
|
66
|
+
sections.push(`\n## Authentication`, `Status: Active`, `Provider: ${provider}`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
sections.push(`\n## Authentication`, `Status: Inactive`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
sections.push(`\n## Authentication`, `Status: Inactive`);
|
|
74
|
+
}
|
|
75
|
+
// Auth page info comes from app-details
|
|
76
|
+
if (appDetailsRaw) {
|
|
77
|
+
const appDetails = YAML.parse(appDetailsRaw);
|
|
78
|
+
const authPageInfo = appDetails.authPageInfo;
|
|
79
|
+
if (authPageInfo) {
|
|
80
|
+
const homeRef = authPageInfo.homePageNodeKeyRef?.key;
|
|
81
|
+
const signInRef = authPageInfo.signInPageNodeKeyRef?.key;
|
|
82
|
+
if (homeRef) {
|
|
83
|
+
const name = await resolvePageName(projectId, homeRef);
|
|
84
|
+
sections.push(`Home page: ${name} (${homeRef})`);
|
|
85
|
+
}
|
|
86
|
+
if (signInRef) {
|
|
87
|
+
const name = await resolvePageName(projectId, signInRef);
|
|
88
|
+
sections.push(`Sign-in page: ${name} (${signInRef})`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// --- Nav Bar ---
|
|
93
|
+
if (navBarRaw) {
|
|
94
|
+
const navBar = YAML.parse(navBarRaw);
|
|
95
|
+
const visible = navBar.show === true;
|
|
96
|
+
if (visible) {
|
|
97
|
+
const navType = navBar.navBarType || "UNKNOWN";
|
|
98
|
+
const labels = navBar.labels === true;
|
|
99
|
+
const pageRefs = navBar.pageKeyRefOrder;
|
|
100
|
+
sections.push(`\n## Nav Bar`, `Visible: Yes`, `Type: ${navType}`, `Labels: ${labels ? "Yes" : "No"}`);
|
|
101
|
+
if (pageRefs && pageRefs.length > 0) {
|
|
102
|
+
const tabs = [];
|
|
103
|
+
for (let i = 0; i < pageRefs.length; i++) {
|
|
104
|
+
const scaffoldId = pageRefs[i].key;
|
|
105
|
+
const name = await resolvePageName(projectId, scaffoldId);
|
|
106
|
+
tabs.push(` ${i + 1}. ${name} (${scaffoldId})`);
|
|
107
|
+
}
|
|
108
|
+
sections.push(`Tabs:`, ...tabs);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
sections.push(`\n## Nav Bar`, `Visible: No`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
sections.push(`\n## Nav Bar`, `Visible: No`);
|
|
117
|
+
}
|
|
118
|
+
// --- Permissions ---
|
|
119
|
+
if (permissionsRaw) {
|
|
120
|
+
const permissions = YAML.parse(permissionsRaw);
|
|
121
|
+
const builtIn = permissions.permissionMessages;
|
|
122
|
+
const custom = permissions.userDefinedPermissions;
|
|
123
|
+
const hasBuiltIn = builtIn && builtIn.length > 0;
|
|
124
|
+
const hasCustom = custom && custom.length > 0;
|
|
125
|
+
if (hasBuiltIn || hasCustom) {
|
|
126
|
+
sections.push(`\n## Permissions`);
|
|
127
|
+
if (hasBuiltIn) {
|
|
128
|
+
sections.push(`Built-in:`);
|
|
129
|
+
for (const perm of builtIn) {
|
|
130
|
+
const permType = perm.permissionType || "UNKNOWN";
|
|
131
|
+
const msg = perm.message?.textValue?.inputValue;
|
|
132
|
+
sections.push(` - ${permType}: ${msg ? `"${msg}"` : "(no message)"}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (hasCustom) {
|
|
136
|
+
sections.push(`Custom:`);
|
|
137
|
+
for (const perm of custom) {
|
|
138
|
+
const names = perm.names;
|
|
139
|
+
const iosName = names?.iosName || "?";
|
|
140
|
+
const androidName = names?.androidName || "?";
|
|
141
|
+
const msg = perm.message?.textValue?.inputValue;
|
|
142
|
+
sections.push(` - ${iosName} / ${androidName}: ${msg ? `"${msg}"` : "(no message)"}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// --- Services ---
|
|
148
|
+
sections.push(`\n## Services`);
|
|
149
|
+
if (revenueCatRaw) {
|
|
150
|
+
const revenueCat = YAML.parse(revenueCatRaw);
|
|
151
|
+
sections.push(`RevenueCat: ${revenueCat.enabled === true ? "enabled" : "disabled"}`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
sections.push(`RevenueCat: disabled`);
|
|
155
|
+
}
|
|
156
|
+
// --- Lifecycle Actions (main.dart) ---
|
|
157
|
+
sections.push(`\n## Lifecycle Actions (main.dart)`);
|
|
158
|
+
const mainFileRaw = await cacheRead(projectId, "custom-file/id-MAIN");
|
|
159
|
+
if (mainFileRaw) {
|
|
160
|
+
const mainFile = YAML.parse(mainFileRaw);
|
|
161
|
+
const actions = mainFile.actions;
|
|
162
|
+
if (actions && actions.length > 0) {
|
|
163
|
+
const initialActions = actions.filter((a) => a.type === "INITIAL_ACTION");
|
|
164
|
+
const finalActions = actions.filter((a) => a.type === "FINAL_ACTION");
|
|
165
|
+
const formatAction = (action, index) => {
|
|
166
|
+
const identifier = action.identifier;
|
|
167
|
+
const name = identifier?.name || "(unnamed)";
|
|
168
|
+
const key = identifier?.key;
|
|
169
|
+
const fromProject = identifier?.projectId;
|
|
170
|
+
const fromSuffix = fromProject ? `, from: ${fromProject}` : "";
|
|
171
|
+
return ` ${index + 1}. ${name} (key: ${key}${fromSuffix})`;
|
|
172
|
+
};
|
|
173
|
+
if (initialActions.length > 0) {
|
|
174
|
+
sections.push(`Initial Actions:`);
|
|
175
|
+
initialActions.forEach((a, i) => sections.push(formatAction(a, i)));
|
|
176
|
+
}
|
|
177
|
+
if (finalActions.length > 0) {
|
|
178
|
+
sections.push(`Final Actions:`);
|
|
179
|
+
finalActions.forEach((a, i) => sections.push(formatAction(a, i)));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
sections.push(`(none)`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
sections.push(`(none)`);
|
|
188
|
+
}
|
|
189
|
+
// --- Project File Map ---
|
|
190
|
+
const allKeys = await listCachedKeys(projectId);
|
|
191
|
+
sections.push(`\n## Project File Map`);
|
|
192
|
+
const categoryPatterns = [
|
|
193
|
+
{ label: "Pages", pattern: /^page\/id-[^/]+$/ },
|
|
194
|
+
{ label: "Components", pattern: /^component\/id-[^/]+$/ },
|
|
195
|
+
{ label: "Custom Actions", pattern: /^custom-actions\/id-[^/]+$/ },
|
|
196
|
+
{ label: "Custom Functions", pattern: /^custom-functions\/id-[^/]+$/ },
|
|
197
|
+
{ label: "Custom Widgets", pattern: /^custom-widgets\/id-[^/]+$/ },
|
|
198
|
+
{ label: "Custom Files", pattern: /^custom-file\/id-[^/]+$/ },
|
|
199
|
+
{ label: "App Action Components", pattern: /^app-action-components\/id-[^/]+$/ },
|
|
200
|
+
{ label: "API Endpoints", prefix: "api-endpoint/" },
|
|
201
|
+
{ label: "Collections", prefix: "collections/" },
|
|
202
|
+
{ label: "AI Agents", pattern: /^agent\/id-[^/]+$/ },
|
|
203
|
+
];
|
|
204
|
+
for (const cat of categoryPatterns) {
|
|
205
|
+
const count = cat.pattern
|
|
206
|
+
? allKeys.filter((k) => cat.pattern.test(k)).length
|
|
207
|
+
: allKeys.filter((k) => k.startsWith(cat.prefix)).length;
|
|
208
|
+
if (count > 0) {
|
|
209
|
+
sections.push(`${cat.label}: ${count}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: sections.join("\n") }],
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
import { cacheRead, cacheMeta, cacheAgeFooter } from "../utils/cache.js";
|
|
4
|
+
import { resolveDataType } from "../utils/resolve-data-type.js";
|
|
5
|
+
export function registerGetProjectSetupTool(server) {
|
|
6
|
+
server.tool("get_project_setup", "Get Project Setup settings — Firebase services, Languages, Platforms, Permissions, Project Dependencies, Dev Environments. Mirrors the FlutterFlow 'Project Setup' settings section. Cache-based, no API calls. Run sync_project first.", {
|
|
7
|
+
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
8
|
+
}, async ({ projectId }) => {
|
|
9
|
+
const meta = await cacheMeta(projectId);
|
|
10
|
+
if (!meta) {
|
|
11
|
+
return {
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: "text",
|
|
15
|
+
text: `No cache found for project "${projectId}". Run sync_project first.`,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const [analyticsRaw, appCheckRaw, crashlyticsRaw, perfMonRaw, remoteConfigRaw, languagesRaw, platformsRaw, permissionsRaw, dependenciesRaw, customCodeDepsRaw, envSettingsRaw,] = await Promise.all([
|
|
21
|
+
cacheRead(projectId, "firebase-analytics"),
|
|
22
|
+
cacheRead(projectId, "firebase-app-check"),
|
|
23
|
+
cacheRead(projectId, "firebase-crashlytics"),
|
|
24
|
+
cacheRead(projectId, "firebase-performance-monitoring"),
|
|
25
|
+
cacheRead(projectId, "firebase-remote-config"),
|
|
26
|
+
cacheRead(projectId, "languages"),
|
|
27
|
+
cacheRead(projectId, "platforms"),
|
|
28
|
+
cacheRead(projectId, "permissions"),
|
|
29
|
+
cacheRead(projectId, "dependencies"),
|
|
30
|
+
cacheRead(projectId, "custom-code-dependencies"),
|
|
31
|
+
cacheRead(projectId, "environment-settings"),
|
|
32
|
+
]);
|
|
33
|
+
const sections = ["# Project Setup"];
|
|
34
|
+
// --- Firebase ---
|
|
35
|
+
sections.push(`\n## Firebase`);
|
|
36
|
+
const anyFirebase = analyticsRaw || appCheckRaw || crashlyticsRaw || perfMonRaw || remoteConfigRaw;
|
|
37
|
+
if (anyFirebase) {
|
|
38
|
+
// Analytics
|
|
39
|
+
if (analyticsRaw) {
|
|
40
|
+
const analytics = YAML.parse(analyticsRaw);
|
|
41
|
+
const enabled = analytics.enabled === true;
|
|
42
|
+
sections.push(`Analytics: ${enabled ? "enabled" : "disabled"}`);
|
|
43
|
+
if (enabled) {
|
|
44
|
+
const eventSettings = analytics.automaticEventSettings;
|
|
45
|
+
if (eventSettings) {
|
|
46
|
+
sections.push(` onPageLoad: ${eventSettings.onPageLoad === true ? "Yes" : "No"}`, ` onActionsStart: ${eventSettings.onActionsStart === true ? "Yes" : "No"}`, ` onAuth: ${eventSettings.onAuth === true ? "Yes" : "No"}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// App Check
|
|
51
|
+
if (appCheckRaw) {
|
|
52
|
+
const appCheck = YAML.parse(appCheckRaw);
|
|
53
|
+
sections.push(`App Check: ${appCheck.enabled === true ? "enabled" : "disabled"}`);
|
|
54
|
+
}
|
|
55
|
+
// Crashlytics
|
|
56
|
+
if (crashlyticsRaw) {
|
|
57
|
+
const crashlytics = YAML.parse(crashlyticsRaw);
|
|
58
|
+
sections.push(`Crashlytics: ${crashlytics.enabled === true ? "enabled" : "disabled"}`);
|
|
59
|
+
}
|
|
60
|
+
// Performance Monitoring
|
|
61
|
+
if (perfMonRaw) {
|
|
62
|
+
const perfMon = YAML.parse(perfMonRaw);
|
|
63
|
+
sections.push(`Performance Monitoring: ${perfMon.enabled === true ? "enabled" : "disabled"}`);
|
|
64
|
+
}
|
|
65
|
+
// Remote Config
|
|
66
|
+
if (remoteConfigRaw) {
|
|
67
|
+
const remoteConfig = YAML.parse(remoteConfigRaw);
|
|
68
|
+
const enabled = remoteConfig.enabled === true;
|
|
69
|
+
sections.push(`Remote Config: ${enabled ? "enabled" : "disabled"}`);
|
|
70
|
+
if (enabled) {
|
|
71
|
+
const fields = remoteConfig.fields || [];
|
|
72
|
+
if (fields.length > 0) {
|
|
73
|
+
sections.push(` Fields:`);
|
|
74
|
+
for (const field of fields) {
|
|
75
|
+
const name = field.parameter?.identifier?.name || "unknown";
|
|
76
|
+
const dt = resolveDataType(field.parameter?.dataType || {});
|
|
77
|
+
const defaultVal = field.serializedDefaultValue ?? "";
|
|
78
|
+
sections.push(` - ${name}: ${dt} (default: "${defaultVal}")`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
sections.push(`(not configured)`);
|
|
86
|
+
}
|
|
87
|
+
// --- Languages ---
|
|
88
|
+
sections.push(`\n## Languages`);
|
|
89
|
+
if (languagesRaw) {
|
|
90
|
+
const langs = YAML.parse(languagesRaw);
|
|
91
|
+
const primaryLang = langs.primaryLanguage
|
|
92
|
+
?.language;
|
|
93
|
+
const displayLang = langs.displayLanguage
|
|
94
|
+
?.language;
|
|
95
|
+
const allLangs = langs.languages || [];
|
|
96
|
+
sections.push(`Primary: ${primaryLang || "not set"}`);
|
|
97
|
+
sections.push(`Display: ${displayLang || "not set"}`);
|
|
98
|
+
if (allLangs.length > 0) {
|
|
99
|
+
const langCodes = allLangs.map((l) => l.language).join(", ");
|
|
100
|
+
sections.push(`Available: ${langCodes}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
sections.push(`(not configured)`);
|
|
105
|
+
}
|
|
106
|
+
// --- Platforms ---
|
|
107
|
+
sections.push(`\n## Platforms`);
|
|
108
|
+
if (platformsRaw) {
|
|
109
|
+
const platforms = YAML.parse(platformsRaw);
|
|
110
|
+
sections.push(`Web: ${platforms.enableWeb === true ? "enabled" : "disabled"}`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
sections.push(`(not configured)`);
|
|
114
|
+
}
|
|
115
|
+
// --- Permissions ---
|
|
116
|
+
sections.push(`\n## Permissions`);
|
|
117
|
+
if (permissionsRaw) {
|
|
118
|
+
const permissions = YAML.parse(permissionsRaw);
|
|
119
|
+
const builtIn = permissions.permissionMessages;
|
|
120
|
+
const custom = permissions.userDefinedPermissions;
|
|
121
|
+
const hasBuiltIn = builtIn && builtIn.length > 0;
|
|
122
|
+
const hasCustom = custom && custom.length > 0;
|
|
123
|
+
if (hasBuiltIn || hasCustom) {
|
|
124
|
+
if (hasBuiltIn) {
|
|
125
|
+
sections.push(`Built-in:`);
|
|
126
|
+
for (const perm of builtIn) {
|
|
127
|
+
const permType = perm.permissionType || "UNKNOWN";
|
|
128
|
+
const msg = perm.message?.textValue?.inputValue;
|
|
129
|
+
sections.push(` - ${permType}: ${msg ? `"${msg}"` : "(no message)"}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (hasCustom) {
|
|
133
|
+
sections.push(`Custom:`);
|
|
134
|
+
for (const perm of custom) {
|
|
135
|
+
const names = perm.names;
|
|
136
|
+
const iosName = names?.iosName || "?";
|
|
137
|
+
const androidName = names?.androidName || "?";
|
|
138
|
+
const msg = perm.message?.textValue?.inputValue;
|
|
139
|
+
sections.push(` - ${iosName} / ${androidName}: ${msg ? `"${msg}"` : "(no message)"}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
sections.push(`(not configured)`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
sections.push(`(not configured)`);
|
|
149
|
+
}
|
|
150
|
+
// --- Project Dependencies ---
|
|
151
|
+
sections.push(`\n## Project Dependencies`);
|
|
152
|
+
const hasDeps = dependenciesRaw || customCodeDepsRaw;
|
|
153
|
+
if (hasDeps) {
|
|
154
|
+
// FF library dependencies
|
|
155
|
+
if (dependenciesRaw) {
|
|
156
|
+
const deps = YAML.parse(dependenciesRaw);
|
|
157
|
+
const libDeps = deps.dependencies || [];
|
|
158
|
+
if (libDeps.length > 0) {
|
|
159
|
+
sections.push(`FF Libraries:`);
|
|
160
|
+
for (const dep of libDeps) {
|
|
161
|
+
sections.push(` - ${dep.name || "unnamed"} (${dep.projectId || "?"}, version: ${dep.version || "?"})`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Pubspec dependencies
|
|
166
|
+
if (customCodeDepsRaw) {
|
|
167
|
+
const customDeps = YAML.parse(customCodeDepsRaw);
|
|
168
|
+
const pubspecDeps = customDeps.pubspecDependencies || [];
|
|
169
|
+
if (pubspecDeps.length > 0) {
|
|
170
|
+
sections.push(`Pubspec Dependencies:`);
|
|
171
|
+
for (const dep of pubspecDeps) {
|
|
172
|
+
sections.push(` - ${dep.name || "unnamed"}: ${dep.version || "any"}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
sections.push(`(not configured)`);
|
|
179
|
+
}
|
|
180
|
+
// --- Dev Environments ---
|
|
181
|
+
sections.push(`\n## Dev Environments`);
|
|
182
|
+
if (envSettingsRaw) {
|
|
183
|
+
const doc = YAML.parse(envSettingsRaw);
|
|
184
|
+
const currentEnv = doc.currentEnvironment;
|
|
185
|
+
const envValues = doc.environmentValues || [];
|
|
186
|
+
if (currentEnv) {
|
|
187
|
+
sections.push(`Current: ${currentEnv.name || "unknown"} (${currentEnv.key || "?"})`);
|
|
188
|
+
}
|
|
189
|
+
const lines = [];
|
|
190
|
+
for (const ev of envValues) {
|
|
191
|
+
const name = ev.parameter?.identifier?.name || "unknown";
|
|
192
|
+
const dt = resolveDataType(ev.parameter?.dataType || {});
|
|
193
|
+
const privateTag = ev.isPrivate ? " (private)" : "";
|
|
194
|
+
lines.push(`- ${name}: ${dt}${privateTag}`);
|
|
195
|
+
const valuesMap = ev.valuesMap || {};
|
|
196
|
+
for (const [envKey, envVal] of Object.entries(valuesMap)) {
|
|
197
|
+
const display = ev.isPrivate ? "****" : (envVal.serializedValue ?? "");
|
|
198
|
+
lines.push(` ${envKey}: ${display}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (lines.length > 0) {
|
|
202
|
+
sections.push(lines.join("\n"));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
sections.push(`(not configured)`);
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
content: [{ type: "text", text: sections.join("\n") + cacheAgeFooter(meta) }],
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
}
|