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.
Files changed (116) hide show
  1. package/README.md +300 -0
  2. package/build/api/flutterflow.d.ts +11 -0
  3. package/build/api/flutterflow.js +61 -0
  4. package/build/index.d.ts +2 -0
  5. package/build/index.js +78 -0
  6. package/build/prompts/dev-workflow.d.ts +2 -0
  7. package/build/prompts/dev-workflow.js +68 -0
  8. package/build/prompts/generate-page.d.ts +2 -0
  9. package/build/prompts/generate-page.js +37 -0
  10. package/build/prompts/inspect-project.d.ts +2 -0
  11. package/build/prompts/inspect-project.js +30 -0
  12. package/build/prompts/modify-component.d.ts +2 -0
  13. package/build/prompts/modify-component.js +40 -0
  14. package/build/resources/docs.d.ts +2 -0
  15. package/build/resources/docs.js +76 -0
  16. package/build/resources/projects.d.ts +3 -0
  17. package/build/resources/projects.js +60 -0
  18. package/build/tools/find-component-usages.d.ts +21 -0
  19. package/build/tools/find-component-usages.js +216 -0
  20. package/build/tools/find-page-navigations.d.ts +26 -0
  21. package/build/tools/find-page-navigations.js +220 -0
  22. package/build/tools/get-api-endpoints.d.ts +2 -0
  23. package/build/tools/get-api-endpoints.js +126 -0
  24. package/build/tools/get-app-settings.d.ts +2 -0
  25. package/build/tools/get-app-settings.js +169 -0
  26. package/build/tools/get-app-state.d.ts +2 -0
  27. package/build/tools/get-app-state.js +96 -0
  28. package/build/tools/get-component-summary.d.ts +22 -0
  29. package/build/tools/get-component-summary.js +195 -0
  30. package/build/tools/get-custom-code.d.ts +2 -0
  31. package/build/tools/get-custom-code.js +380 -0
  32. package/build/tools/get-data-models.d.ts +2 -0
  33. package/build/tools/get-data-models.js +266 -0
  34. package/build/tools/get-editing-guide.d.ts +7 -0
  35. package/build/tools/get-editing-guide.js +185 -0
  36. package/build/tools/get-general-settings.d.ts +2 -0
  37. package/build/tools/get-general-settings.js +116 -0
  38. package/build/tools/get-in-app-purchases.d.ts +2 -0
  39. package/build/tools/get-in-app-purchases.js +51 -0
  40. package/build/tools/get-integrations.d.ts +2 -0
  41. package/build/tools/get-integrations.js +137 -0
  42. package/build/tools/get-page-by-name.d.ts +3 -0
  43. package/build/tools/get-page-by-name.js +56 -0
  44. package/build/tools/get-page-summary.d.ts +22 -0
  45. package/build/tools/get-page-summary.js +205 -0
  46. package/build/tools/get-project-config.d.ts +2 -0
  47. package/build/tools/get-project-config.js +216 -0
  48. package/build/tools/get-project-setup.d.ts +2 -0
  49. package/build/tools/get-project-setup.js +212 -0
  50. package/build/tools/get-theme.d.ts +2 -0
  51. package/build/tools/get-theme.js +199 -0
  52. package/build/tools/get-yaml-docs.d.ts +6 -0
  53. package/build/tools/get-yaml-docs.js +116 -0
  54. package/build/tools/get-yaml.d.ts +2 -0
  55. package/build/tools/get-yaml.js +53 -0
  56. package/build/tools/list-files.d.ts +3 -0
  57. package/build/tools/list-files.js +49 -0
  58. package/build/tools/list-pages.d.ts +25 -0
  59. package/build/tools/list-pages.js +101 -0
  60. package/build/tools/list-projects.d.ts +3 -0
  61. package/build/tools/list-projects.js +23 -0
  62. package/build/tools/search-project-files.d.ts +2 -0
  63. package/build/tools/search-project-files.js +69 -0
  64. package/build/tools/sync-project.d.ts +3 -0
  65. package/build/tools/sync-project.js +147 -0
  66. package/build/tools/update-yaml.d.ts +3 -0
  67. package/build/tools/update-yaml.js +24 -0
  68. package/build/tools/validate-yaml.d.ts +3 -0
  69. package/build/tools/validate-yaml.js +39 -0
  70. package/build/utils/batch-process.d.ts +2 -0
  71. package/build/utils/batch-process.js +10 -0
  72. package/build/utils/cache.d.ts +58 -0
  73. package/build/utils/cache.js +199 -0
  74. package/build/utils/decode-yaml.d.ts +7 -0
  75. package/build/utils/decode-yaml.js +31 -0
  76. package/build/utils/page-summary/action-summarizer.d.ts +24 -0
  77. package/build/utils/page-summary/action-summarizer.js +291 -0
  78. package/build/utils/page-summary/formatter.d.ts +13 -0
  79. package/build/utils/page-summary/formatter.js +129 -0
  80. package/build/utils/page-summary/node-extractor.d.ts +24 -0
  81. package/build/utils/page-summary/node-extractor.js +227 -0
  82. package/build/utils/page-summary/tree-walker.d.ts +6 -0
  83. package/build/utils/page-summary/tree-walker.js +55 -0
  84. package/build/utils/page-summary/types.d.ts +58 -0
  85. package/build/utils/page-summary/types.js +4 -0
  86. package/build/utils/parse-folders.d.ts +9 -0
  87. package/build/utils/parse-folders.js +29 -0
  88. package/build/utils/resolve-data-type.d.ts +2 -0
  89. package/build/utils/resolve-data-type.js +18 -0
  90. package/build/utils/topic-map.d.ts +7 -0
  91. package/build/utils/topic-map.js +122 -0
  92. package/docs/ff-yaml/00-overview.md +166 -0
  93. package/docs/ff-yaml/01-project-files.md +2309 -0
  94. package/docs/ff-yaml/02-pages.md +572 -0
  95. package/docs/ff-yaml/03-components.md +784 -0
  96. package/docs/ff-yaml/04-widgets/README.md +122 -0
  97. package/docs/ff-yaml/04-widgets/button.md +444 -0
  98. package/docs/ff-yaml/04-widgets/container.md +358 -0
  99. package/docs/ff-yaml/04-widgets/dropdown.md +579 -0
  100. package/docs/ff-yaml/04-widgets/form.md +256 -0
  101. package/docs/ff-yaml/04-widgets/image.md +276 -0
  102. package/docs/ff-yaml/04-widgets/layout.md +355 -0
  103. package/docs/ff-yaml/04-widgets/misc.md +553 -0
  104. package/docs/ff-yaml/04-widgets/text-field.md +326 -0
  105. package/docs/ff-yaml/04-widgets/text.md +302 -0
  106. package/docs/ff-yaml/05-actions.md +953 -0
  107. package/docs/ff-yaml/06-variables.md +849 -0
  108. package/docs/ff-yaml/07-data.md +591 -0
  109. package/docs/ff-yaml/08-custom-code.md +736 -0
  110. package/docs/ff-yaml/09-theming.md +638 -0
  111. package/docs/ff-yaml/10-editing-guide.md +497 -0
  112. package/docs/ff-yaml/README.md +105 -0
  113. package/package.json +59 -0
  114. package/skills/community-ff-mcp/SKILL.md +201 -0
  115. package/skills/ff-widget-patterns.md +141 -0
  116. package/skills/ff-yaml-dev.md +70 -0
@@ -0,0 +1,126 @@
1
+ import { z } from "zod";
2
+ import YAML from "yaml";
3
+ import { cacheRead, cacheMeta, cacheAgeFooter, 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) + cacheAgeFooter(meta) }],
124
+ };
125
+ });
126
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerGetAppSettingsTool(server: McpServer): void;
@@ -0,0 +1,169 @@
1
+ import { z } from "zod";
2
+ import YAML from "yaml";
3
+ import { cacheRead, cacheMeta, cacheAgeFooter } 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") + cacheAgeFooter(meta) }],
167
+ };
168
+ });
169
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerGetAppStateTool(server: McpServer): void;
@@ -0,0 +1,96 @@
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 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") + cacheAgeFooter(meta) }],
94
+ };
95
+ });
96
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * get_component_summary tool — assembles cached sub-files into a readable
3
+ * component summary. Zero API calls: everything comes from the local .ff-cache.
4
+ */
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ interface ResolvedComponent {
7
+ containerId: string;
8
+ componentFileKey: string;
9
+ }
10
+ /**
11
+ * Resolve a component name or container ID to its cache file key.
12
+ * Returns all available component names if no match found.
13
+ */
14
+ export declare function resolveComponent(projectId: string, componentName?: string, componentId?: string): Promise<{
15
+ ok: true;
16
+ component: ResolvedComponent;
17
+ } | {
18
+ ok: false;
19
+ available: string[];
20
+ }>;
21
+ export declare function registerGetComponentSummaryTool(server: McpServer): void;
22
+ export {};
@@ -0,0 +1,195 @@
1
+ import { z } from "zod";
2
+ import YAML from "yaml";
3
+ import { cacheRead, cacheMeta, cacheAgeFooter, listCachedKeys, } from "../utils/cache.js";
4
+ import { parseTreeOutline } from "../utils/page-summary/tree-walker.js";
5
+ import { extractNodeInfo } from "../utils/page-summary/node-extractor.js";
6
+ import { summarizeTriggers } from "../utils/page-summary/action-summarizer.js";
7
+ import { formatComponentSummary } from "../utils/page-summary/formatter.js";
8
+ /**
9
+ * Resolve a component name or container ID to its cache file key.
10
+ * Returns all available component names if no match found.
11
+ */
12
+ export async function resolveComponent(projectId, componentName, componentId) {
13
+ if (componentId) {
14
+ const componentFileKey = `component/id-${componentId}`;
15
+ const content = await cacheRead(projectId, componentFileKey);
16
+ if (content) {
17
+ return { ok: true, component: { containerId: componentId, componentFileKey } };
18
+ }
19
+ }
20
+ // List all cached component top-level files
21
+ const allKeys = await listCachedKeys(projectId, "component/id-");
22
+ // Filter to top-level component files only (not sub-files)
23
+ const componentKeys = allKeys.filter((k) => /^component\/id-Container_\w+$/.test(k));
24
+ const available = [];
25
+ for (const key of componentKeys) {
26
+ const content = await cacheRead(projectId, key);
27
+ if (!content)
28
+ continue;
29
+ const nameMatch = content.match(/^name:\s*(.+)$/m);
30
+ const name = nameMatch ? nameMatch[1].trim() : "";
31
+ if (componentName && name.toLowerCase() === componentName.toLowerCase()) {
32
+ const cid = key.match(/^component\/id-(Container_\w+)$/)?.[1] || "";
33
+ return { ok: true, component: { containerId: cid, componentFileKey: key } };
34
+ }
35
+ if (name)
36
+ available.push(name);
37
+ }
38
+ return { ok: false, available };
39
+ }
40
+ // ---------------------------------------------------------------------------
41
+ // Metadata extraction
42
+ // ---------------------------------------------------------------------------
43
+ /** Resolve a scalar/list data type to a readable string. */
44
+ function resolveDataType(dt) {
45
+ if (dt.listType) {
46
+ const inner = dt.listType;
47
+ return `List<${inner.scalarType || "unknown"}>`;
48
+ }
49
+ if (dt.scalarType === "DataStruct") {
50
+ const sub = dt.subType;
51
+ const dsi = sub?.dataStructIdentifier;
52
+ return dsi?.name ? `DataStruct:${dsi.name}` : "DataStruct";
53
+ }
54
+ if (dt.enumType) {
55
+ const en = dt.enumType;
56
+ const eid = en.enumIdentifier;
57
+ return eid?.name ? `Enum:${eid.name}` : "Enum";
58
+ }
59
+ return dt.scalarType || "unknown";
60
+ }
61
+ /** Extract component metadata (name, description, params) from the top-level YAML. */
62
+ async function extractComponentMeta(projectId, component) {
63
+ const content = await cacheRead(projectId, component.componentFileKey);
64
+ if (!content) {
65
+ return {
66
+ componentName: component.containerId,
67
+ containerId: component.containerId,
68
+ description: "",
69
+ params: [],
70
+ };
71
+ }
72
+ const doc = YAML.parse(content);
73
+ const componentName = doc.name || component.containerId;
74
+ const description = doc.description || "";
75
+ // Params
76
+ const params = [];
77
+ const rawParams = doc.params;
78
+ if (rawParams) {
79
+ for (const val of Object.values(rawParams)) {
80
+ const id = val.identifier;
81
+ const name = id?.name || "unknown";
82
+ const dt = val.dataType || {};
83
+ const defaultVal = val.defaultValue;
84
+ params.push({
85
+ name,
86
+ dataType: resolveDataType(dt),
87
+ defaultValue: defaultVal?.serializedValue,
88
+ });
89
+ }
90
+ }
91
+ return { componentName, containerId: component.containerId, description, params };
92
+ }
93
+ // ---------------------------------------------------------------------------
94
+ // Tree enrichment (reused from page summary)
95
+ // ---------------------------------------------------------------------------
96
+ /**
97
+ * Recursively enrich an OutlineNode tree with node info and trigger summaries.
98
+ */
99
+ async function enrichNode(projectId, treePrefix, // e.g. "component/id-Container_xxx/component-widget-tree-outline"
100
+ outline) {
101
+ const nodeInfo = await extractNodeInfo(projectId, treePrefix, outline.key);
102
+ // Check for triggers
103
+ const nodeFilePrefix = `${treePrefix}/node/id-${outline.key}`;
104
+ const triggers = await summarizeTriggers(projectId, nodeFilePrefix);
105
+ // Enrich children
106
+ const children = await Promise.all(outline.children.map((child) => enrichNode(projectId, treePrefix, child)));
107
+ return {
108
+ key: outline.key,
109
+ type: nodeInfo.type,
110
+ name: nodeInfo.name,
111
+ slot: outline.slot,
112
+ detail: nodeInfo.detail,
113
+ componentRef: nodeInfo.componentRef,
114
+ componentId: nodeInfo.componentId,
115
+ triggers,
116
+ children,
117
+ };
118
+ }
119
+ // ---------------------------------------------------------------------------
120
+ // Tool registration
121
+ // ---------------------------------------------------------------------------
122
+ export function registerGetComponentSummaryTool(server) {
123
+ server.tool("get_component_summary", "Get a readable summary of a FlutterFlow component from local cache — widget tree, actions, params. Nested component references are resolved to show [ComponentName] (ComponentId). No API calls. Run sync_project first if not cached.", {
124
+ projectId: z.string().describe("The FlutterFlow project ID"),
125
+ componentName: z
126
+ .string()
127
+ .optional()
128
+ .describe("Human-readable component name (e.g. 'PremuimContentWall'). Case-insensitive. Provide either componentName or componentId."),
129
+ componentId: z
130
+ .string()
131
+ .optional()
132
+ .describe("Container ID (e.g. 'Container_ffzg5wc5'). Provide either componentName or componentId."),
133
+ }, async ({ projectId, componentName, componentId }) => {
134
+ // Check cache exists
135
+ const meta = await cacheMeta(projectId);
136
+ if (!meta) {
137
+ return {
138
+ content: [
139
+ {
140
+ type: "text",
141
+ text: `No cache found for project "${projectId}". Run sync_project first to download the project YAML files.`,
142
+ },
143
+ ],
144
+ };
145
+ }
146
+ if (!componentName && !componentId) {
147
+ return {
148
+ content: [
149
+ {
150
+ type: "text",
151
+ text: "Provide either componentName or componentId.",
152
+ },
153
+ ],
154
+ };
155
+ }
156
+ // Resolve component
157
+ const resolved = await resolveComponent(projectId, componentName, componentId);
158
+ if (!resolved.ok) {
159
+ const list = resolved.available.map((n) => ` - ${n}`).join("\n");
160
+ const searchTerm = componentName || componentId || "";
161
+ return {
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: `Component "${searchTerm}" not found in cache. Available components:\n${list}`,
166
+ },
167
+ ],
168
+ };
169
+ }
170
+ const component = resolved.component;
171
+ // Extract metadata
172
+ const componentMeta = await extractComponentMeta(projectId, component);
173
+ // Parse widget tree outline
174
+ const outlineKey = `${component.componentFileKey}/component-widget-tree-outline`;
175
+ const outlineContent = await cacheRead(projectId, outlineKey);
176
+ if (!outlineContent) {
177
+ return {
178
+ content: [
179
+ {
180
+ type: "text",
181
+ text: `Component "${componentMeta.componentName}" found but widget tree outline is not cached. Re-run sync_project to fetch all sub-files.`,
182
+ },
183
+ ],
184
+ };
185
+ }
186
+ const outlineTree = parseTreeOutline(outlineContent);
187
+ // Enrich the tree with node info and triggers
188
+ const enrichedTree = await enrichNode(projectId, outlineKey, outlineTree);
189
+ // Format output
190
+ const summary = formatComponentSummary(componentMeta, enrichedTree);
191
+ return {
192
+ content: [{ type: "text", text: summary + cacheAgeFooter(meta) }],
193
+ };
194
+ });
195
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerGetCustomCodeTool(server: McpServer): void;