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
package/build/index.js
CHANGED
|
@@ -21,6 +21,18 @@ import { registerModifyComponentPrompt } from "./prompts/modify-component.js";
|
|
|
21
21
|
import { registerInspectProjectPrompt } from "./prompts/inspect-project.js";
|
|
22
22
|
import { registerDevWorkflowPrompt } from "./prompts/dev-workflow.js";
|
|
23
23
|
import { registerGetYamlDocsTool } from "./tools/get-yaml-docs.js";
|
|
24
|
+
import { registerGetAppStateTool } from "./tools/get-app-state.js";
|
|
25
|
+
import { registerGetApiEndpointsTool } from "./tools/get-api-endpoints.js";
|
|
26
|
+
import { registerGetDataModelsTool } from "./tools/get-data-models.js";
|
|
27
|
+
import { registerGetCustomCodeTool } from "./tools/get-custom-code.js";
|
|
28
|
+
import { registerGetThemeTool } from "./tools/get-theme.js";
|
|
29
|
+
import { registerGetEditingGuideTool } from "./tools/get-editing-guide.js";
|
|
30
|
+
import { registerSearchProjectFilesTool } from "./tools/search-project-files.js";
|
|
31
|
+
import { registerGetGeneralSettingsTool } from "./tools/get-general-settings.js";
|
|
32
|
+
import { registerGetProjectSetupTool } from "./tools/get-project-setup.js";
|
|
33
|
+
import { registerGetAppSettingsTool } from "./tools/get-app-settings.js";
|
|
34
|
+
import { registerGetInAppPurchasesTool } from "./tools/get-in-app-purchases.js";
|
|
35
|
+
import { registerGetIntegrationsTool } from "./tools/get-integrations.js";
|
|
24
36
|
const server = new McpServer({
|
|
25
37
|
name: "ff-mcp",
|
|
26
38
|
version: "0.1.0",
|
|
@@ -40,6 +52,18 @@ registerGetComponentSummaryTool(server);
|
|
|
40
52
|
registerFindComponentUsagesTool(server);
|
|
41
53
|
registerFindPageNavigationsTool(server);
|
|
42
54
|
registerGetYamlDocsTool(server);
|
|
55
|
+
registerGetAppStateTool(server);
|
|
56
|
+
registerGetApiEndpointsTool(server);
|
|
57
|
+
registerGetDataModelsTool(server);
|
|
58
|
+
registerGetCustomCodeTool(server);
|
|
59
|
+
registerGetThemeTool(server);
|
|
60
|
+
registerGetEditingGuideTool(server);
|
|
61
|
+
registerSearchProjectFilesTool(server);
|
|
62
|
+
registerGetGeneralSettingsTool(server);
|
|
63
|
+
registerGetProjectSetupTool(server);
|
|
64
|
+
registerGetAppSettingsTool(server);
|
|
65
|
+
registerGetInAppPurchasesTool(server);
|
|
66
|
+
registerGetIntegrationsTool(server);
|
|
43
67
|
// Register resources
|
|
44
68
|
registerResources(server, client);
|
|
45
69
|
registerDocsResources(server);
|
|
@@ -16,14 +16,15 @@ export function registerGeneratePagePrompt(server) {
|
|
|
16
16
|
## Instructions
|
|
17
17
|
|
|
18
18
|
1. First, use the list_project_files tool with projectId "${projectId}" to understand the existing project structure.
|
|
19
|
-
2.
|
|
20
|
-
3.
|
|
19
|
+
2. Use \`get_editing_guide\` with a description of the page you want to create to get the correct YAML schemas, workflow steps, and critical rules.
|
|
20
|
+
3. Then, use get_project_yaml to read a few existing pages to understand the YAML schema and conventions used in this project.
|
|
21
|
+
4. Based on the following description, generate valid FlutterFlow YAML for a new page:
|
|
21
22
|
|
|
22
23
|
**Page Description:** ${description}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
5. Use validate_yaml to check your generated YAML is valid.
|
|
26
|
+
6. If validation passes, use update_project_yaml to push the new page to the project.
|
|
27
|
+
7. If validation fails, fix the errors and try again.
|
|
27
28
|
|
|
28
29
|
## Important
|
|
29
30
|
- Follow the exact YAML structure you observed in existing pages.
|
|
@@ -19,14 +19,15 @@ export function registerModifyComponentPrompt(server) {
|
|
|
19
19
|
## Instructions
|
|
20
20
|
|
|
21
21
|
1. Use get_project_yaml with projectId "${projectId}" and fileName "${fileName}" to read the current component YAML.
|
|
22
|
-
2.
|
|
23
|
-
3.
|
|
22
|
+
2. Use \`get_editing_guide\` with a description of the changes you want to make to get the correct YAML schemas and critical rules.
|
|
23
|
+
3. Understand the current structure and widget tree.
|
|
24
|
+
4. Apply the following changes:
|
|
24
25
|
|
|
25
26
|
**Requested Changes:** ${changes}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
5. Use validate_yaml to verify your modified YAML is valid.
|
|
29
|
+
6. If validation passes, use update_project_yaml to push the changes.
|
|
30
|
+
7. If validation fails, fix the errors and try again.
|
|
30
31
|
|
|
31
32
|
## Important
|
|
32
33
|
- Preserve all existing structure you are not modifying.
|
|
@@ -2,19 +2,10 @@ import { z } from "zod";
|
|
|
2
2
|
import YAML from "yaml";
|
|
3
3
|
import { cacheRead, cacheMeta, listCachedKeys, } from "../utils/cache.js";
|
|
4
4
|
import { resolveComponent } from "./get-component-summary.js";
|
|
5
|
+
import { batchProcess } from "../utils/batch-process.js";
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Helpers
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
|
-
/** Batch-process items in groups to avoid overwhelming the file system. */
|
|
9
|
-
async function batchProcess(items, batchSize, fn) {
|
|
10
|
-
const results = [];
|
|
11
|
-
for (let i = 0; i < items.length; i += batchSize) {
|
|
12
|
-
const batch = items.slice(i, i + batchSize);
|
|
13
|
-
const batchResults = await Promise.all(batch.map(fn));
|
|
14
|
-
results.push(...batchResults);
|
|
15
|
-
}
|
|
16
|
-
return results;
|
|
17
|
-
}
|
|
18
9
|
/**
|
|
19
10
|
* Resolve the value of a parameter pass to a readable string.
|
|
20
11
|
*/
|
|
@@ -2,18 +2,10 @@ import { z } from "zod";
|
|
|
2
2
|
import YAML from "yaml";
|
|
3
3
|
import { cacheRead, cacheMeta, listCachedKeys, } from "../utils/cache.js";
|
|
4
4
|
import { resolvePage } from "./get-page-summary.js";
|
|
5
|
+
import { batchProcess } from "../utils/batch-process.js";
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Helpers
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
|
-
async function batchProcess(items, batchSize, fn) {
|
|
9
|
-
const results = [];
|
|
10
|
-
for (let i = 0; i < items.length; i += batchSize) {
|
|
11
|
-
const batch = items.slice(i, i + batchSize);
|
|
12
|
-
const batchResults = await Promise.all(batch.map(fn));
|
|
13
|
-
results.push(...batchResults);
|
|
14
|
-
}
|
|
15
|
-
return results;
|
|
16
|
-
}
|
|
17
9
|
/**
|
|
18
10
|
* Parse parent context from an action file key.
|
|
19
11
|
* Example: "page/id-Scaffold_XXX/page-widget-tree-outline/node/id-Widget_YYY/trigger_actions/id-ON_TAP/action/id-zzz"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
import { cacheRead, cacheMeta, listCachedKeys } from "../utils/cache.js";
|
|
4
|
+
import { batchProcess } from "../utils/batch-process.js";
|
|
5
|
+
function parseEndpoint(content) {
|
|
6
|
+
const doc = YAML.parse(content);
|
|
7
|
+
if (!doc)
|
|
8
|
+
return null;
|
|
9
|
+
const identifier = doc.identifier;
|
|
10
|
+
const name = identifier?.name || "Unknown";
|
|
11
|
+
const method = doc.callType || "GET";
|
|
12
|
+
const url = doc.url || "";
|
|
13
|
+
const bodyType = doc.bodyType;
|
|
14
|
+
const body = doc.body;
|
|
15
|
+
const variables = [];
|
|
16
|
+
const rawVars = doc.variables;
|
|
17
|
+
if (Array.isArray(rawVars)) {
|
|
18
|
+
for (const v of rawVars) {
|
|
19
|
+
const vid = v.identifier;
|
|
20
|
+
const vName = vid?.name || "unknown";
|
|
21
|
+
const vType = v.type || "String";
|
|
22
|
+
variables.push({ name: vName, type: vType });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const headers = [];
|
|
26
|
+
const rawHeaders = doc.headers;
|
|
27
|
+
if (Array.isArray(rawHeaders)) {
|
|
28
|
+
headers.push(...rawHeaders);
|
|
29
|
+
}
|
|
30
|
+
const responseFields = [];
|
|
31
|
+
const rawJsonPaths = doc.jsonPathDefinitions;
|
|
32
|
+
if (Array.isArray(rawJsonPaths)) {
|
|
33
|
+
for (const jp of rawJsonPaths) {
|
|
34
|
+
const jpId = jp.identifier;
|
|
35
|
+
const jpName = jpId?.name || "unknown";
|
|
36
|
+
const jpDef = jp.jsonPath;
|
|
37
|
+
const path = jpDef?.jsonPath || "";
|
|
38
|
+
const returnParam = jpDef?.returnParameter;
|
|
39
|
+
const dt = returnParam?.dataType;
|
|
40
|
+
const scalarType = dt?.scalarType || "String";
|
|
41
|
+
responseFields.push({ name: jpName, type: scalarType, jsonPath: path });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { name, method, url, bodyType, body, variables, headers, responseFields };
|
|
45
|
+
}
|
|
46
|
+
function formatEndpoints(endpoints) {
|
|
47
|
+
if (endpoints.length === 0)
|
|
48
|
+
return "No API endpoints found in cache.";
|
|
49
|
+
const lines = [`# API Endpoints (${endpoints.length})`];
|
|
50
|
+
for (const ep of endpoints) {
|
|
51
|
+
lines.push("");
|
|
52
|
+
lines.push(`## ${ep.name}`);
|
|
53
|
+
lines.push(`Method: ${ep.method}`);
|
|
54
|
+
lines.push(`URL: ${ep.url}`);
|
|
55
|
+
if (ep.bodyType)
|
|
56
|
+
lines.push(`Body type: ${ep.bodyType}`);
|
|
57
|
+
if (ep.variables.length > 0) {
|
|
58
|
+
lines.push("Variables:");
|
|
59
|
+
for (const v of ep.variables) {
|
|
60
|
+
lines.push(` - ${v.name}: ${v.type}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (ep.headers.length > 0) {
|
|
64
|
+
lines.push("Headers:");
|
|
65
|
+
for (const h of ep.headers) {
|
|
66
|
+
lines.push(` - ${h}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (ep.responseFields.length > 0) {
|
|
70
|
+
lines.push("Response fields:");
|
|
71
|
+
for (const rf of ep.responseFields) {
|
|
72
|
+
lines.push(` - ${rf.name}: ${rf.type} (${rf.jsonPath})`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
|
78
|
+
export function registerGetApiEndpointsTool(server) {
|
|
79
|
+
server.tool("get_api_endpoints", "Get API endpoint definitions from local cache — method, URL, variables, headers, response fields. No API calls. Run sync_project first if not cached.", {
|
|
80
|
+
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
81
|
+
name: z
|
|
82
|
+
.string()
|
|
83
|
+
.optional()
|
|
84
|
+
.describe("Case-insensitive filter on endpoint name"),
|
|
85
|
+
}, async ({ projectId, name }) => {
|
|
86
|
+
const meta = await cacheMeta(projectId);
|
|
87
|
+
if (!meta) {
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: `No cache found for project "${projectId}". Run sync_project first to download the project YAML files.`,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const allKeys = await listCachedKeys(projectId, "api-endpoint/id-");
|
|
98
|
+
const topLevelKeys = allKeys.filter((k) => /^api-endpoint\/id-[a-z0-9]+$/i.test(k));
|
|
99
|
+
const parsed = await batchProcess(topLevelKeys, 10, async (key) => {
|
|
100
|
+
const content = await cacheRead(projectId, key);
|
|
101
|
+
if (!content)
|
|
102
|
+
return null;
|
|
103
|
+
return parseEndpoint(content);
|
|
104
|
+
});
|
|
105
|
+
let endpoints = parsed.filter((ep) => ep !== null);
|
|
106
|
+
if (name) {
|
|
107
|
+
const lower = name.toLowerCase();
|
|
108
|
+
const filtered = endpoints.filter((ep) => ep.name.toLowerCase().includes(lower));
|
|
109
|
+
if (filtered.length === 0) {
|
|
110
|
+
const available = endpoints.map((ep) => ep.name).join(", ");
|
|
111
|
+
return {
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: "text",
|
|
115
|
+
text: `No API endpoints matching '${name}' found. Available: ${available}`,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
endpoints = filtered;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: "text", text: formatEndpoints(endpoints) }],
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
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 registerGetAppSettingsTool(server) {
|
|
12
|
+
server.tool("get_app_settings", "Get App Settings — Authentication, Push Notifications, Mobile Deployment (version, build, stores), Web Deployment (SEO, title). Mirrors the FlutterFlow 'App 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 [authRaw, appDetailsRaw, pushRaw, mobileRaw, webRaw] = await Promise.all([
|
|
27
|
+
cacheRead(projectId, "authentication"),
|
|
28
|
+
cacheRead(projectId, "app-details"),
|
|
29
|
+
cacheRead(projectId, "push-notifications"),
|
|
30
|
+
cacheRead(projectId, "mobile-deployment"),
|
|
31
|
+
cacheRead(projectId, "web-publishing"),
|
|
32
|
+
]);
|
|
33
|
+
const sections = ["# App Settings"];
|
|
34
|
+
// --- Authentication ---
|
|
35
|
+
sections.push(`\n## Authentication`);
|
|
36
|
+
if (authRaw) {
|
|
37
|
+
const auth = YAML.parse(authRaw);
|
|
38
|
+
const active = auth.active === true;
|
|
39
|
+
if (active) {
|
|
40
|
+
let provider = "Unknown";
|
|
41
|
+
const firebaseConfigs = auth.firebaseConfigFileInfos;
|
|
42
|
+
const supabase = auth.supabase;
|
|
43
|
+
if (firebaseConfigs && firebaseConfigs.length > 0) {
|
|
44
|
+
provider = "Firebase";
|
|
45
|
+
}
|
|
46
|
+
else if (supabase && Object.keys(supabase).length > 0) {
|
|
47
|
+
provider = "Supabase";
|
|
48
|
+
}
|
|
49
|
+
sections.push(`Status: Active`, `Provider: ${provider}`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
sections.push(`Status: Inactive`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
sections.push(`Status: Inactive`);
|
|
57
|
+
}
|
|
58
|
+
// Auth page info from app-details
|
|
59
|
+
if (appDetailsRaw) {
|
|
60
|
+
const appDetails = YAML.parse(appDetailsRaw);
|
|
61
|
+
const authPageInfo = appDetails.authPageInfo;
|
|
62
|
+
if (authPageInfo) {
|
|
63
|
+
const homeRef = authPageInfo.homePageNodeKeyRef?.key;
|
|
64
|
+
const signInRef = authPageInfo.signInPageNodeKeyRef?.key;
|
|
65
|
+
if (homeRef) {
|
|
66
|
+
const name = await resolvePageName(projectId, homeRef);
|
|
67
|
+
sections.push(`Home page: ${name} (${homeRef})`);
|
|
68
|
+
}
|
|
69
|
+
if (signInRef) {
|
|
70
|
+
const name = await resolvePageName(projectId, signInRef);
|
|
71
|
+
sections.push(`Sign-in page: ${name} (${signInRef})`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// --- Push Notifications ---
|
|
76
|
+
sections.push(`\n## Push Notifications`);
|
|
77
|
+
if (pushRaw) {
|
|
78
|
+
const push = YAML.parse(pushRaw);
|
|
79
|
+
const enabled = push.enabled === true;
|
|
80
|
+
const allowScheduled = push.allowScheduledNotifications === true;
|
|
81
|
+
const autoPrompt = push.autoPromptUsersForNotificationsPermission === true;
|
|
82
|
+
sections.push(`Enabled: ${enabled ? "Yes" : "No"}`, `Scheduled notifications: ${allowScheduled ? "Yes" : "No"}`, `Auto-prompt permission: ${autoPrompt ? "Yes" : "No"}`);
|
|
83
|
+
const lastNotif = push.lastNotificationSent;
|
|
84
|
+
if (lastNotif) {
|
|
85
|
+
const title = lastNotif.notificationTitle || "(untitled)";
|
|
86
|
+
const status = lastNotif.status || "unknown";
|
|
87
|
+
sections.push(`Last notification: "${title}" (${status})`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
sections.push(`(not configured)`);
|
|
92
|
+
}
|
|
93
|
+
// --- Mobile Deployment ---
|
|
94
|
+
sections.push(`\n## Mobile Deployment`);
|
|
95
|
+
if (mobileRaw) {
|
|
96
|
+
const mobile = YAML.parse(mobileRaw);
|
|
97
|
+
const settingsMap = mobile.codemagicSettingsMap;
|
|
98
|
+
if (settingsMap && Object.keys(settingsMap).length > 0) {
|
|
99
|
+
// Prefer PROD, fall back to first key
|
|
100
|
+
const envKey = settingsMap.PROD
|
|
101
|
+
? "PROD"
|
|
102
|
+
: Object.keys(settingsMap)[0];
|
|
103
|
+
const envSettings = settingsMap[envKey];
|
|
104
|
+
sections.push(`Environment: ${envKey}`);
|
|
105
|
+
// Build version
|
|
106
|
+
const buildVersion = envSettings.buildVersion;
|
|
107
|
+
if (buildVersion) {
|
|
108
|
+
if (buildVersion.buildVersion) {
|
|
109
|
+
sections.push(`Version: ${buildVersion.buildVersion}`);
|
|
110
|
+
}
|
|
111
|
+
if (buildVersion.buildNumber != null) {
|
|
112
|
+
sections.push(`Build number: ${buildVersion.buildNumber}`);
|
|
113
|
+
}
|
|
114
|
+
if (buildVersion.lastSubmitted) {
|
|
115
|
+
sections.push(`Last submitted: ${buildVersion.lastSubmitted}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Play Store
|
|
119
|
+
const playStore = envSettings.playStoreSettings;
|
|
120
|
+
if (playStore?.playTrack) {
|
|
121
|
+
sections.push(`Play Store track: ${playStore.playTrack}`);
|
|
122
|
+
}
|
|
123
|
+
// App Store — NEVER output private keys
|
|
124
|
+
const appStore = envSettings.appStoreSettings;
|
|
125
|
+
if (appStore?.ascAppId) {
|
|
126
|
+
sections.push(`App Store ID: ${appStore.ascAppId}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
sections.push(`(not configured)`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
sections.push(`(not configured)`);
|
|
135
|
+
}
|
|
136
|
+
// --- Web Deployment ---
|
|
137
|
+
sections.push(`\n## Web Deployment`);
|
|
138
|
+
if (webRaw) {
|
|
139
|
+
const web = YAML.parse(webRaw);
|
|
140
|
+
const webSettings = web.webSettings;
|
|
141
|
+
if (webSettings && Object.keys(webSettings).length > 0) {
|
|
142
|
+
// Prefer PROD, fall back to first key
|
|
143
|
+
const envKey = webSettings.PROD
|
|
144
|
+
? "PROD"
|
|
145
|
+
: Object.keys(webSettings)[0];
|
|
146
|
+
const envData = webSettings[envKey];
|
|
147
|
+
sections.push(`Environment: ${envKey}`);
|
|
148
|
+
if (envData.pageTitle) {
|
|
149
|
+
sections.push(`Page title: ${envData.pageTitle}`);
|
|
150
|
+
}
|
|
151
|
+
if (envData.seoDescription) {
|
|
152
|
+
sections.push(`SEO description: ${envData.seoDescription}`);
|
|
153
|
+
}
|
|
154
|
+
if (envData.orientation) {
|
|
155
|
+
sections.push(`Orientation: ${envData.orientation}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
sections.push(`(not configured)`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
sections.push(`(not configured)`);
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
content: [{ type: "text", text: sections.join("\n") }],
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
import { cacheRead, cacheMeta } from "../utils/cache.js";
|
|
4
|
+
import { resolveDataType } from "../utils/resolve-data-type.js";
|
|
5
|
+
export function registerGetAppStateTool(server) {
|
|
6
|
+
server.tool("get_app_state", "Get app state variables, constants, and environment settings from local cache. No API calls. Run sync_project first if not cached.", {
|
|
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 sections = ["# App State"];
|
|
21
|
+
// --- State Variables ---
|
|
22
|
+
const appStateYaml = await cacheRead(projectId, "app-state");
|
|
23
|
+
if (appStateYaml) {
|
|
24
|
+
const doc = YAML.parse(appStateYaml);
|
|
25
|
+
const fields = doc.fields || [];
|
|
26
|
+
const lines = [];
|
|
27
|
+
for (const field of fields) {
|
|
28
|
+
const name = field.parameter?.identifier?.name || "unknown";
|
|
29
|
+
const dt = resolveDataType(field.parameter?.dataType || {});
|
|
30
|
+
const parts = [`- ${name}: ${dt}`];
|
|
31
|
+
if (field.persisted)
|
|
32
|
+
parts.push("(persisted)");
|
|
33
|
+
if (field.serializedDefaultValue && field.serializedDefaultValue.length > 0) {
|
|
34
|
+
const joined = field.serializedDefaultValue
|
|
35
|
+
.map((v) => `"${v}"`)
|
|
36
|
+
.join(", ");
|
|
37
|
+
parts.push(`[default: ${joined}]`);
|
|
38
|
+
}
|
|
39
|
+
lines.push(parts.join(" "));
|
|
40
|
+
}
|
|
41
|
+
sections.push("\n## State Variables");
|
|
42
|
+
if (lines.length > 0) {
|
|
43
|
+
sections.push(lines.join("\n"));
|
|
44
|
+
}
|
|
45
|
+
const secure = doc.securePersistedValues ?? false;
|
|
46
|
+
sections.push(`\nSecure persisted values: ${secure ? "Yes" : "No"}`);
|
|
47
|
+
}
|
|
48
|
+
// --- Constants ---
|
|
49
|
+
const constantsYaml = await cacheRead(projectId, "app-constants");
|
|
50
|
+
if (constantsYaml) {
|
|
51
|
+
const doc = YAML.parse(constantsYaml);
|
|
52
|
+
const fields = doc.fields || [];
|
|
53
|
+
const lines = [];
|
|
54
|
+
for (const field of fields) {
|
|
55
|
+
const name = field.parameter?.identifier?.name || "unknown";
|
|
56
|
+
const dt = resolveDataType(field.parameter?.dataType || {});
|
|
57
|
+
const vals = field.serializedValue || [];
|
|
58
|
+
const formatted = vals.map((v) => `"${v}"`).join(", ");
|
|
59
|
+
lines.push(`- ${name}: ${dt} = [${formatted}]`);
|
|
60
|
+
}
|
|
61
|
+
sections.push("\n## Constants");
|
|
62
|
+
if (lines.length > 0) {
|
|
63
|
+
sections.push(lines.join("\n"));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// --- Environment Settings ---
|
|
67
|
+
const envYaml = await cacheRead(projectId, "environment-settings");
|
|
68
|
+
if (envYaml) {
|
|
69
|
+
const doc = YAML.parse(envYaml);
|
|
70
|
+
const currentEnv = doc.currentEnvironment;
|
|
71
|
+
const envValues = doc.environmentValues || [];
|
|
72
|
+
sections.push("\n## Environment Settings");
|
|
73
|
+
if (currentEnv) {
|
|
74
|
+
sections.push(`Current: ${currentEnv.name || "unknown"} (${currentEnv.key || "?"})`);
|
|
75
|
+
}
|
|
76
|
+
const lines = [];
|
|
77
|
+
for (const ev of envValues) {
|
|
78
|
+
const name = ev.parameter?.identifier?.name || "unknown";
|
|
79
|
+
const dt = resolveDataType(ev.parameter?.dataType || {});
|
|
80
|
+
const privateTag = ev.isPrivate ? " (private)" : "";
|
|
81
|
+
lines.push(`- ${name}: ${dt}${privateTag}`);
|
|
82
|
+
const valuesMap = ev.valuesMap || {};
|
|
83
|
+
for (const [envKey, envVal] of Object.entries(valuesMap)) {
|
|
84
|
+
const display = ev.isPrivate ? "****" : (envVal.serializedValue ?? "");
|
|
85
|
+
lines.push(` ${envKey}: ${display}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (lines.length > 0) {
|
|
89
|
+
sections.push(lines.join("\n"));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: "text", text: sections.join("\n") }],
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
}
|