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,76 @@
|
|
|
1
|
+
import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
const DOCS_DIR = path.resolve(__dirname, "../../docs/ff-yaml");
|
|
8
|
+
/** Recursively list all .md files under a directory, returning relative paths. */
|
|
9
|
+
function listDocFiles(dir, prefix = "") {
|
|
10
|
+
const results = [];
|
|
11
|
+
if (!fs.existsSync(dir))
|
|
12
|
+
return results;
|
|
13
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
14
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
results.push(...listDocFiles(path.join(dir, entry.name), relPath));
|
|
17
|
+
}
|
|
18
|
+
else if (entry.name.endsWith(".md")) {
|
|
19
|
+
results.push(relPath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return results;
|
|
23
|
+
}
|
|
24
|
+
export function registerDocsResources(server) {
|
|
25
|
+
// Static resource: list all available docs
|
|
26
|
+
server.resource("docs-index", "ff://docs", {
|
|
27
|
+
description: "FlutterFlow YAML reference catalog — list all available documentation files",
|
|
28
|
+
mimeType: "application/json",
|
|
29
|
+
}, async (uri) => {
|
|
30
|
+
const files = listDocFiles(DOCS_DIR);
|
|
31
|
+
const index = files.map((f) => ({
|
|
32
|
+
file: f.replace(/\.md$/, ""),
|
|
33
|
+
uri: `ff://docs/${f.replace(/\.md$/, "")}`,
|
|
34
|
+
}));
|
|
35
|
+
return {
|
|
36
|
+
contents: [
|
|
37
|
+
{
|
|
38
|
+
uri: uri.href,
|
|
39
|
+
text: JSON.stringify(index, null, 2),
|
|
40
|
+
mimeType: "application/json",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
// Dynamic resource: get a specific doc file
|
|
46
|
+
server.resource("docs-file", new ResourceTemplate("ff://docs/{+path}", { list: undefined }), {
|
|
47
|
+
description: "Read a specific FlutterFlow YAML reference doc (e.g. ff://docs/04-widgets/button)",
|
|
48
|
+
mimeType: "text/markdown",
|
|
49
|
+
}, async (uri, { path: docPath }) => {
|
|
50
|
+
const filePath = path.join(DOCS_DIR, `${docPath}.md`);
|
|
51
|
+
if (!fs.existsSync(filePath)) {
|
|
52
|
+
const available = listDocFiles(DOCS_DIR)
|
|
53
|
+
.map((f) => f.replace(/\.md$/, ""))
|
|
54
|
+
.join("\n ");
|
|
55
|
+
return {
|
|
56
|
+
contents: [
|
|
57
|
+
{
|
|
58
|
+
uri: uri.href,
|
|
59
|
+
text: `Doc not found: ${docPath}\n\nAvailable docs:\n ${available}`,
|
|
60
|
+
mimeType: "text/plain",
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
66
|
+
return {
|
|
67
|
+
contents: [
|
|
68
|
+
{
|
|
69
|
+
uri: uri.href,
|
|
70
|
+
text: content,
|
|
71
|
+
mimeType: "text/markdown",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { decodeProjectYamlResponse } from "../utils/decode-yaml.js";
|
|
3
|
+
export function registerResources(server, client) {
|
|
4
|
+
// Static resource: list all projects
|
|
5
|
+
server.resource("projects", "ff://projects", {
|
|
6
|
+
description: "List all FlutterFlow projects",
|
|
7
|
+
mimeType: "application/json",
|
|
8
|
+
}, async (uri) => {
|
|
9
|
+
const projects = await client.listProjects();
|
|
10
|
+
return {
|
|
11
|
+
contents: [
|
|
12
|
+
{
|
|
13
|
+
uri: uri.href,
|
|
14
|
+
text: JSON.stringify(projects, null, 2),
|
|
15
|
+
mimeType: "application/json",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
// Dynamic resource: list files in a project
|
|
21
|
+
server.resource("project-files", new ResourceTemplate("ff://projects/{projectId}/files", {
|
|
22
|
+
list: undefined,
|
|
23
|
+
}), {
|
|
24
|
+
description: "List YAML file names in a FlutterFlow project",
|
|
25
|
+
mimeType: "application/json",
|
|
26
|
+
}, async (uri, { projectId }) => {
|
|
27
|
+
const files = await client.listPartitionedFileNames(projectId);
|
|
28
|
+
return {
|
|
29
|
+
contents: [
|
|
30
|
+
{
|
|
31
|
+
uri: uri.href,
|
|
32
|
+
text: JSON.stringify(files, null, 2),
|
|
33
|
+
mimeType: "application/json",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
// Dynamic resource: get specific YAML file
|
|
39
|
+
server.resource("project-yaml", new ResourceTemplate("ff://projects/{projectId}/yaml/{+fileName}", {
|
|
40
|
+
list: undefined,
|
|
41
|
+
}), {
|
|
42
|
+
description: "Get a specific YAML file from a FlutterFlow project",
|
|
43
|
+
mimeType: "text/yaml",
|
|
44
|
+
}, async (uri, { projectId, fileName }) => {
|
|
45
|
+
const raw = await client.getProjectYamls(projectId, fileName);
|
|
46
|
+
const decoded = decodeProjectYamlResponse(raw);
|
|
47
|
+
const yamlText = Object.entries(decoded)
|
|
48
|
+
.map(([name, content]) => `# ${name}\n${content}`)
|
|
49
|
+
.join("\n");
|
|
50
|
+
return {
|
|
51
|
+
contents: [
|
|
52
|
+
{
|
|
53
|
+
uri: uri.href,
|
|
54
|
+
text: yamlText,
|
|
55
|
+
mimeType: "text/yaml",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* find_component_usages tool — scans cached node files to find all pages and
|
|
3
|
+
* components where a given component is instantiated.
|
|
4
|
+
* Zero API calls: everything comes from the local .ff-cache.
|
|
5
|
+
*/
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the value of a parameter pass to a readable string.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveParamValue(paramObj: Record<string, unknown>): string;
|
|
11
|
+
/**
|
|
12
|
+
* Extract parent context (page or component name + ID) from a file key path.
|
|
13
|
+
* Examples:
|
|
14
|
+
* "page/id-Scaffold_xxx/page-widget-tree-outline/node/id-Widget_yyy"
|
|
15
|
+
* "component/id-Container_xxx/component-widget-tree-outline/node/id-Widget_yyy"
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseParentFromKey(fileKey: string): {
|
|
18
|
+
type: "page" | "component";
|
|
19
|
+
id: string;
|
|
20
|
+
} | null;
|
|
21
|
+
export declare function registerFindComponentUsagesTool(server: McpServer): void;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
import { cacheRead, cacheMeta, cacheAgeFooter, listCachedKeys, } from "../utils/cache.js";
|
|
4
|
+
import { resolveComponent } from "./get-component-summary.js";
|
|
5
|
+
import { batchProcess } from "../utils/batch-process.js";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the value of a parameter pass to a readable string.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveParamValue(paramObj) {
|
|
13
|
+
// Check for variable source (e.g. INTERNATIONALIZATION)
|
|
14
|
+
const variable = paramObj.variable;
|
|
15
|
+
if (variable) {
|
|
16
|
+
const source = variable.source;
|
|
17
|
+
if (source === "INTERNATIONALIZATION") {
|
|
18
|
+
const fc = variable.functionCall;
|
|
19
|
+
const values = fc?.values;
|
|
20
|
+
if (values && values.length > 0) {
|
|
21
|
+
const first = values[0];
|
|
22
|
+
const iv = first.inputValue;
|
|
23
|
+
const sv = iv?.serializedValue;
|
|
24
|
+
return sv ? `"${sv}" (i18n)` : "[i18n]";
|
|
25
|
+
}
|
|
26
|
+
return "[i18n]";
|
|
27
|
+
}
|
|
28
|
+
if (source)
|
|
29
|
+
return `[${source}]`;
|
|
30
|
+
return "[dynamic]";
|
|
31
|
+
}
|
|
32
|
+
// Check for inputValue
|
|
33
|
+
const inputValue = paramObj.inputValue;
|
|
34
|
+
if (inputValue != null) {
|
|
35
|
+
if (typeof inputValue === "string" || typeof inputValue === "number") {
|
|
36
|
+
return `"${inputValue}"`;
|
|
37
|
+
}
|
|
38
|
+
if (typeof inputValue === "object") {
|
|
39
|
+
const ivo = inputValue;
|
|
40
|
+
if ("serializedValue" in ivo)
|
|
41
|
+
return `"${ivo.serializedValue}"`;
|
|
42
|
+
if ("themeColor" in ivo)
|
|
43
|
+
return `[theme:${ivo.themeColor}]`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return "[dynamic]";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Extract parent context (page or component name + ID) from a file key path.
|
|
50
|
+
* Examples:
|
|
51
|
+
* "page/id-Scaffold_xxx/page-widget-tree-outline/node/id-Widget_yyy"
|
|
52
|
+
* "component/id-Container_xxx/component-widget-tree-outline/node/id-Widget_yyy"
|
|
53
|
+
*/
|
|
54
|
+
export function parseParentFromKey(fileKey) {
|
|
55
|
+
const pageMatch = fileKey.match(/^page\/id-(Scaffold_\w+)\//);
|
|
56
|
+
if (pageMatch)
|
|
57
|
+
return { type: "page", id: pageMatch[1] };
|
|
58
|
+
const compMatch = fileKey.match(/^component\/id-(Container_\w+)\//);
|
|
59
|
+
if (compMatch)
|
|
60
|
+
return { type: "component", id: compMatch[1] };
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the name of a page or component from its top-level YAML cache.
|
|
65
|
+
*/
|
|
66
|
+
async function resolveParentName(projectId, parentType, parentId) {
|
|
67
|
+
const prefix = parentType === "page" ? "page" : "component";
|
|
68
|
+
const fileKey = `${prefix}/id-${parentId}`;
|
|
69
|
+
const content = await cacheRead(projectId, fileKey);
|
|
70
|
+
if (!content)
|
|
71
|
+
return parentId;
|
|
72
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
73
|
+
return nameMatch ? nameMatch[1].trim() : parentId;
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Tool registration
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
export function registerFindComponentUsagesTool(server) {
|
|
79
|
+
server.tool("find_component_usages", "Find all pages and components where a given component is used, with parameter pass details. Cache-only, no API calls. Run sync_project first.", {
|
|
80
|
+
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
81
|
+
componentName: z
|
|
82
|
+
.string()
|
|
83
|
+
.optional()
|
|
84
|
+
.describe("Human-readable component name (e.g. 'PremuimContentWall'). Case-insensitive."),
|
|
85
|
+
componentId: z
|
|
86
|
+
.string()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe("Container ID (e.g. 'Container_ffzg5wc5')."),
|
|
89
|
+
}, async ({ projectId, componentName, componentId }) => {
|
|
90
|
+
// Check cache exists
|
|
91
|
+
const meta = await cacheMeta(projectId);
|
|
92
|
+
if (!meta) {
|
|
93
|
+
return {
|
|
94
|
+
content: [
|
|
95
|
+
{
|
|
96
|
+
type: "text",
|
|
97
|
+
text: `No cache found for project "${projectId}". Run sync_project first to download the project YAML files.`,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (!componentName && !componentId) {
|
|
103
|
+
return {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: "Provide either componentName or componentId.",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Resolve component to its key
|
|
113
|
+
const resolved = await resolveComponent(projectId, componentName, componentId);
|
|
114
|
+
if (!resolved.ok) {
|
|
115
|
+
const list = resolved.available.map((n) => ` - ${n}`).join("\n");
|
|
116
|
+
const searchTerm = componentName || componentId || "";
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: "text",
|
|
121
|
+
text: `Component "${searchTerm}" not found in cache. Available components:\n${list}`,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const targetId = resolved.component.containerId;
|
|
127
|
+
// Get component name for display
|
|
128
|
+
const componentContent = await cacheRead(projectId, resolved.component.componentFileKey);
|
|
129
|
+
const displayName = componentContent
|
|
130
|
+
? (componentContent.match(/^name:\s*(.+)$/m)?.[1]?.trim() || targetId)
|
|
131
|
+
: targetId;
|
|
132
|
+
// Scan all node files for componentClassKeyRef matching this component
|
|
133
|
+
const allKeys = await listCachedKeys(projectId);
|
|
134
|
+
const nodeKeys = allKeys.filter((k) => /\/node\/id-[A-Z]/.test(k));
|
|
135
|
+
// Read node files in batches to check for component references
|
|
136
|
+
const usages = [];
|
|
137
|
+
await batchProcess(nodeKeys, 20, async (nodeKey) => {
|
|
138
|
+
const content = await cacheRead(projectId, nodeKey);
|
|
139
|
+
if (!content)
|
|
140
|
+
return;
|
|
141
|
+
// Quick string check before parsing YAML
|
|
142
|
+
if (!content.includes(targetId))
|
|
143
|
+
return;
|
|
144
|
+
let doc;
|
|
145
|
+
try {
|
|
146
|
+
doc = YAML.parse(content);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Check componentClassKeyRef
|
|
152
|
+
const classKeyRef = doc.componentClassKeyRef;
|
|
153
|
+
if (!classKeyRef)
|
|
154
|
+
return;
|
|
155
|
+
const refKey = classKeyRef.key;
|
|
156
|
+
if (refKey !== targetId)
|
|
157
|
+
return;
|
|
158
|
+
// Found a usage! Extract parent context
|
|
159
|
+
const parent = parseParentFromKey(nodeKey);
|
|
160
|
+
if (!parent)
|
|
161
|
+
return;
|
|
162
|
+
// Extract the widget key from the file key
|
|
163
|
+
const widgetKeyMatch = nodeKey.match(/\/node\/id-(\w+)$/);
|
|
164
|
+
const widgetKey = widgetKeyMatch ? widgetKeyMatch[1] : "unknown";
|
|
165
|
+
// Extract parameter passes
|
|
166
|
+
const params = [];
|
|
167
|
+
const parameterValues = doc.parameterValues;
|
|
168
|
+
if (parameterValues) {
|
|
169
|
+
for (const [, paramVal] of Object.entries(parameterValues)) {
|
|
170
|
+
const paramId = paramVal.paramIdentifier;
|
|
171
|
+
const value = resolveParamValue(paramVal);
|
|
172
|
+
params.push({
|
|
173
|
+
paramName: paramId || "unknown",
|
|
174
|
+
value,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Resolve parent name
|
|
179
|
+
const parentName = await resolveParentName(projectId, parent.type, parent.id);
|
|
180
|
+
usages.push({
|
|
181
|
+
parentType: parent.type,
|
|
182
|
+
parentName,
|
|
183
|
+
parentId: parent.id,
|
|
184
|
+
widgetKey,
|
|
185
|
+
params,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
// Format output
|
|
189
|
+
if (usages.length === 0) {
|
|
190
|
+
return {
|
|
191
|
+
content: [
|
|
192
|
+
{
|
|
193
|
+
type: "text",
|
|
194
|
+
text: `Component: ${displayName} (${targetId})\nNo usages found in cached files.`,
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const lines = [];
|
|
200
|
+
lines.push(`Component: ${displayName} (${targetId})`);
|
|
201
|
+
lines.push(`Found ${usages.length} usage${usages.length === 1 ? "" : "s"}:`);
|
|
202
|
+
lines.push("");
|
|
203
|
+
for (let i = 0; i < usages.length; i++) {
|
|
204
|
+
const u = usages[i];
|
|
205
|
+
const parentLabel = u.parentType === "page" ? u.parentName : `[component] ${u.parentName}`;
|
|
206
|
+
lines.push(`${i + 1}. ${parentLabel} (${u.parentId}) \u2192 ${u.widgetKey}`);
|
|
207
|
+
if (u.params.length > 0) {
|
|
208
|
+
const paramStrs = u.params.map((p) => `${p.paramName} = ${p.value}`);
|
|
209
|
+
lines.push(` Params: ${paramStrs.join(", ")}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: lines.join("\n") + cacheAgeFooter(meta) }],
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* find_page_navigations tool — scans cached action files to find all places
|
|
3
|
+
* that navigate to a given page.
|
|
4
|
+
* Zero API calls: everything comes from the local .ff-cache.
|
|
5
|
+
*/
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
/**
|
|
8
|
+
* Parse parent context from an action file key.
|
|
9
|
+
* Example: "page/id-Scaffold_XXX/page-widget-tree-outline/node/id-Widget_YYY/trigger_actions/id-ON_TAP/action/id-zzz"
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseActionContext(fileKey: string): {
|
|
12
|
+
parentType: "page" | "component";
|
|
13
|
+
parentId: string;
|
|
14
|
+
widgetKey: string;
|
|
15
|
+
trigger: string;
|
|
16
|
+
} | null;
|
|
17
|
+
/**
|
|
18
|
+
* Recursively search an object for a navigate action targeting the given scaffold ID.
|
|
19
|
+
* Returns navigate details if found, including whether it's inside a disableAction.
|
|
20
|
+
*/
|
|
21
|
+
export declare function findNavigateAction(obj: unknown, targetScaffoldId: string, isDisabled: boolean, depth: number): {
|
|
22
|
+
disabled: boolean;
|
|
23
|
+
allowBack: boolean;
|
|
24
|
+
passedParams: string[];
|
|
25
|
+
} | null;
|
|
26
|
+
export declare function registerFindPageNavigationsTool(server: McpServer): void;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
import { cacheRead, cacheMeta, cacheAgeFooter, listCachedKeys, } from "../utils/cache.js";
|
|
4
|
+
import { resolvePage } from "./get-page-summary.js";
|
|
5
|
+
import { batchProcess } from "../utils/batch-process.js";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
/**
|
|
10
|
+
* Parse parent context from an action file key.
|
|
11
|
+
* Example: "page/id-Scaffold_XXX/page-widget-tree-outline/node/id-Widget_YYY/trigger_actions/id-ON_TAP/action/id-zzz"
|
|
12
|
+
*/
|
|
13
|
+
export function parseActionContext(fileKey) {
|
|
14
|
+
// Page action
|
|
15
|
+
const pageMatch = fileKey.match(/^page\/id-(Scaffold_\w+)\/.*\/node\/id-(\w+)\/trigger_actions\/id-([^/]+)\/action\//);
|
|
16
|
+
if (pageMatch) {
|
|
17
|
+
return {
|
|
18
|
+
parentType: "page",
|
|
19
|
+
parentId: pageMatch[1],
|
|
20
|
+
widgetKey: pageMatch[2],
|
|
21
|
+
trigger: pageMatch[3],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// Component action
|
|
25
|
+
const compMatch = fileKey.match(/^component\/id-(Container_\w+)\/.*\/node\/id-(\w+)\/trigger_actions\/id-([^/]+)\/action\//);
|
|
26
|
+
if (compMatch) {
|
|
27
|
+
return {
|
|
28
|
+
parentType: "component",
|
|
29
|
+
parentId: compMatch[1],
|
|
30
|
+
widgetKey: compMatch[2],
|
|
31
|
+
trigger: compMatch[3],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Recursively search an object for a navigate action targeting the given scaffold ID.
|
|
38
|
+
* Returns navigate details if found, including whether it's inside a disableAction.
|
|
39
|
+
*/
|
|
40
|
+
export function findNavigateAction(obj, targetScaffoldId, isDisabled, depth) {
|
|
41
|
+
if (!obj || typeof obj !== "object" || depth > 12)
|
|
42
|
+
return null;
|
|
43
|
+
const o = obj;
|
|
44
|
+
// Check for disableAction wrapper
|
|
45
|
+
if ("disableAction" in o) {
|
|
46
|
+
const da = o.disableAction;
|
|
47
|
+
return findNavigateAction(da, targetScaffoldId, true, depth + 1);
|
|
48
|
+
}
|
|
49
|
+
// Check for navigate action with matching pageNodeKeyRef
|
|
50
|
+
if ("navigate" in o) {
|
|
51
|
+
const nav = o.navigate;
|
|
52
|
+
const pageRef = nav.pageNodeKeyRef;
|
|
53
|
+
if (pageRef?.key === targetScaffoldId) {
|
|
54
|
+
const allowBack = nav.allowBack ?? true;
|
|
55
|
+
const passedParams = [];
|
|
56
|
+
const params = nav.passedParameters;
|
|
57
|
+
if (params) {
|
|
58
|
+
for (const [key, val] of Object.entries(params)) {
|
|
59
|
+
if (key === "widgetClassNodeKeyRef")
|
|
60
|
+
continue;
|
|
61
|
+
passedParams.push(key);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { disabled: isDisabled, allowBack, passedParams };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Recurse into values
|
|
68
|
+
for (const val of Object.values(o)) {
|
|
69
|
+
if (val && typeof val === "object") {
|
|
70
|
+
const found = findNavigateAction(val, targetScaffoldId, isDisabled, depth + 1);
|
|
71
|
+
if (found)
|
|
72
|
+
return found;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the name of a page or component from its top-level YAML cache.
|
|
79
|
+
*/
|
|
80
|
+
async function resolveParentName(projectId, parentType, parentId) {
|
|
81
|
+
const prefix = parentType === "page" ? "page" : "component";
|
|
82
|
+
const fileKey = `${prefix}/id-${parentId}`;
|
|
83
|
+
const content = await cacheRead(projectId, fileKey);
|
|
84
|
+
if (!content)
|
|
85
|
+
return parentId;
|
|
86
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
87
|
+
return nameMatch ? nameMatch[1].trim() : parentId;
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Tool registration
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
export function registerFindPageNavigationsTool(server) {
|
|
93
|
+
server.tool("find_page_navigations", "Find all actions that navigate to a given page, showing source page, trigger, disabled status, and passed parameters. Cache-only, no API calls. Run sync_project first.", {
|
|
94
|
+
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
95
|
+
pageName: z
|
|
96
|
+
.string()
|
|
97
|
+
.optional()
|
|
98
|
+
.describe("Human-readable page name (e.g. 'PaywallPage'). Case-insensitive."),
|
|
99
|
+
scaffoldId: z
|
|
100
|
+
.string()
|
|
101
|
+
.optional()
|
|
102
|
+
.describe("Scaffold ID (e.g. 'Scaffold_tydsj8ql')."),
|
|
103
|
+
}, async ({ projectId, pageName, scaffoldId }) => {
|
|
104
|
+
// Check cache exists
|
|
105
|
+
const meta = await cacheMeta(projectId);
|
|
106
|
+
if (!meta) {
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: `No cache found for project "${projectId}". Run sync_project first to download the project YAML files.`,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (!pageName && !scaffoldId) {
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: "text",
|
|
121
|
+
text: "Provide either pageName or scaffoldId.",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Resolve page
|
|
127
|
+
const resolved = await resolvePage(projectId, pageName, scaffoldId);
|
|
128
|
+
if (!resolved.ok) {
|
|
129
|
+
const list = resolved.available.map((n) => ` - ${n}`).join("\n");
|
|
130
|
+
const searchTerm = pageName || scaffoldId || "";
|
|
131
|
+
return {
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: "text",
|
|
135
|
+
text: `Page "${searchTerm}" not found in cache. Available pages:\n${list}`,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const targetScaffoldId = resolved.page.scaffoldId;
|
|
141
|
+
// Get target page name for display
|
|
142
|
+
const targetContent = await cacheRead(projectId, resolved.page.pageFileKey);
|
|
143
|
+
const displayName = targetContent
|
|
144
|
+
? (targetContent.match(/^name:\s*(.+)$/m)?.[1]?.trim() || targetScaffoldId)
|
|
145
|
+
: targetScaffoldId;
|
|
146
|
+
// Scan all action files for navigate references
|
|
147
|
+
const allKeys = await listCachedKeys(projectId);
|
|
148
|
+
const actionKeys = allKeys.filter((k) => /\/action\/id-/.test(k));
|
|
149
|
+
const refs = [];
|
|
150
|
+
await batchProcess(actionKeys, 20, async (actionKey) => {
|
|
151
|
+
const content = await cacheRead(projectId, actionKey);
|
|
152
|
+
if (!content)
|
|
153
|
+
return;
|
|
154
|
+
// Quick string check before YAML parsing
|
|
155
|
+
if (!content.includes(targetScaffoldId))
|
|
156
|
+
return;
|
|
157
|
+
let doc;
|
|
158
|
+
try {
|
|
159
|
+
doc = YAML.parse(content);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const navInfo = findNavigateAction(doc, targetScaffoldId, false, 0);
|
|
165
|
+
if (!navInfo)
|
|
166
|
+
return;
|
|
167
|
+
const ctx = parseActionContext(actionKey);
|
|
168
|
+
if (!ctx)
|
|
169
|
+
return;
|
|
170
|
+
const parentName = await resolveParentName(projectId, ctx.parentType, ctx.parentId);
|
|
171
|
+
refs.push({
|
|
172
|
+
parentType: ctx.parentType,
|
|
173
|
+
parentName,
|
|
174
|
+
parentId: ctx.parentId,
|
|
175
|
+
widgetKey: ctx.widgetKey,
|
|
176
|
+
trigger: ctx.trigger,
|
|
177
|
+
disabled: navInfo.disabled,
|
|
178
|
+
allowBack: navInfo.allowBack,
|
|
179
|
+
passedParams: navInfo.passedParams,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
// Deduplicate (same parent + widget + trigger)
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
const uniqueRefs = refs.filter((r) => {
|
|
185
|
+
const key = `${r.parentId}:${r.widgetKey}:${r.trigger}:${r.disabled}`;
|
|
186
|
+
if (seen.has(key))
|
|
187
|
+
return false;
|
|
188
|
+
seen.add(key);
|
|
189
|
+
return true;
|
|
190
|
+
});
|
|
191
|
+
// Format output
|
|
192
|
+
if (uniqueRefs.length === 0) {
|
|
193
|
+
return {
|
|
194
|
+
content: [
|
|
195
|
+
{
|
|
196
|
+
type: "text",
|
|
197
|
+
text: `Page: ${displayName} (${targetScaffoldId})\nNo navigations found in cached action files.`,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const lines = [];
|
|
203
|
+
lines.push(`Page: ${displayName} (${targetScaffoldId})`);
|
|
204
|
+
lines.push(`Found ${uniqueRefs.length} navigation${uniqueRefs.length === 1 ? "" : "s"}:`);
|
|
205
|
+
lines.push("");
|
|
206
|
+
for (let i = 0; i < uniqueRefs.length; i++) {
|
|
207
|
+
const r = uniqueRefs[i];
|
|
208
|
+
const status = r.disabled ? "[DISABLED] " : "";
|
|
209
|
+
const parentLabel = r.parentType === "page" ? r.parentName : `[component] ${r.parentName}`;
|
|
210
|
+
const back = r.allowBack ? "" : " (no back)";
|
|
211
|
+
lines.push(`${i + 1}. ${status}${parentLabel} (${r.parentId}) → ${r.trigger} on ${r.widgetKey}${back}`);
|
|
212
|
+
if (r.passedParams.length > 0) {
|
|
213
|
+
lines.push(` Params: ${r.passedParams.join(", ")}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
content: [{ type: "text", text: lines.join("\n") + cacheAgeFooter(meta) }],
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
}
|