community-ff-mcp 0.4.0 → 0.4.2
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
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# community-ff-mcp
|
|
2
2
|
|
|
3
|
+
> **Disclaimer:** This is an unofficial, community-built project. It is not affiliated with, endorsed by, or sponsored by FlutterFlow. "FlutterFlow" is a trademark of FlutterFlow Inc. This project uses the FlutterFlow public API.
|
|
4
|
+
|
|
3
5
|
MCP server for the FlutterFlow Project API. Enables AI-assisted FlutterFlow development through Claude and other MCP-compatible clients.
|
|
4
6
|
|
|
5
7
|
## What This MCP Does Best
|
|
@@ -265,13 +267,33 @@ See [docs/ff-yaml/](docs/ff-yaml/) for the full catalog.
|
|
|
265
267
|
|
|
266
268
|
## AI Agent Skill
|
|
267
269
|
|
|
268
|
-
This MCP includes a [skills.sh](https://skills.sh)-compatible skill
|
|
270
|
+
This MCP includes a [skills.sh](https://skills.sh)-compatible skill that teaches AI assistants how to use FlutterFlow MCP effectively. The skill provides tool orchestration workflows, critical YAML rules, and reference documentation for widgets, actions, data binding, theming, and more.
|
|
271
|
+
|
|
272
|
+
### Install the Skill
|
|
269
273
|
|
|
270
274
|
```bash
|
|
271
275
|
npx skills add mohn93/ff-mcp --skill community-ff-mcp
|
|
272
276
|
```
|
|
273
277
|
|
|
274
|
-
|
|
278
|
+
This works with **18+ AI agents** including Claude Code, Cursor, GitHub Copilot, Codex, Goose, Windsurf, and more. The `skills` CLI auto-detects your AI tool and installs the skill to the correct location.
|
|
279
|
+
|
|
280
|
+
### What the Skill Teaches Your AI
|
|
281
|
+
|
|
282
|
+
Once installed, your AI assistant will know how to:
|
|
283
|
+
|
|
284
|
+
- **Discover** — List projects, pages, components, and search project files
|
|
285
|
+
- **Read** — Summarize pages/components, trace component usages, inspect navigations
|
|
286
|
+
- **Edit** — Follow the correct read → guide → edit → validate → push workflow
|
|
287
|
+
- **Configure** — Read theme, app state, API endpoints, data models, and integrations
|
|
288
|
+
|
|
289
|
+
### Skill + MCP Setup
|
|
290
|
+
|
|
291
|
+
The skill and the MCP server are installed separately:
|
|
292
|
+
|
|
293
|
+
1. **MCP server** gives the AI the tools (see [Quick Start](#quick-start) above)
|
|
294
|
+
2. **Skill** teaches the AI *how* to use those tools effectively
|
|
295
|
+
|
|
296
|
+
If the skill detects the MCP tools aren't connected, it will guide the user through setup automatically.
|
|
275
297
|
|
|
276
298
|
## Claude Code Skills
|
|
277
299
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "community-ff-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "MCP server for the FlutterFlow Project API — AI-assisted FlutterFlow development through Claude and other MCP-compatible clients",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -1,216 +0,0 @@
|
|
|
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
|
-
}
|