Skip to main content
Glama

Convex MCP server

Official
by get-convex
generateFileTree.ts7.16 kB
import { AnalyzedModuleFunction, Module, } from "system-udfs/convex/_system/frontend/common"; import { Id } from "system-udfs/convex/_generated/dataModel"; import { test } from "fuzzy"; import { ComponentId, Nent } from "@common/lib/useNents"; import { File, FileOrFolder, Folder, ModuleFunction, } from "@common/lib/functions/types"; export const ROOT_PATH = ""; export function functionIdentifierValue( identifier: string, componentPath?: string, componentId?: string, ) { return JSON.stringify({ identifier, componentPath, componentId }); } export function functionIdentifierFromValue(value: string) { return JSON.parse(value) as { identifier: string; componentPath?: string; componentId?: Id<"_components">; }; } export function generateFileTreeAllNents( modulesAllNents: Map<ComponentId | null, Map<string, Module>>, nents: Nent[] | undefined, searchTerm: string = "", ): Map<string, File | Folder | ModuleFunction> { if (!modulesAllNents) { return new Map(); } const appTree = generateFileTree( new Map(modulesAllNents.get(null)?.entries() ?? []), null, null, searchTerm, ); const tree = new Map(); for (const [path, node] of appTree.entries()) { tree.set(functionIdentifierValue(path), node); } for (const [componentId, modulesInNent] of modulesAllNents.entries()) { const nent = nents?.find((n) => n.id === componentId); if (componentId !== null && !!nent) { const nentPath = nent.path; const componentTree = generateFileTree( modulesInNent, componentId, nentPath, searchTerm, ); for (const node of componentTree.values()) { node.componentPath = nentPath; node.componentId = componentId; tree.set( functionIdentifierValue( node.identifier, node.componentPath, node.componentId, ), node, ); } } else { // Skip unmounted components in tree. } } return tree; } function moduleDisplayName(filePath: string) { // First, ensure all parent directories exist. const pathComponents = filePath.split("/"); if (!pathComponents.length) { throw new Error(`Invalid module path: ${filePath}`); } // Module display name is the last component of the path without the // extension const displayModuleName = pathComponents[pathComponents.length - 1].split(".")[0]; return displayModuleName; } export function generateFileTree( modules: Map<string, Module>, componentId: ComponentId, componentPath: string | null, searchTerm: string = "", ): Map<string, File | Folder | ModuleFunction> { const nodes: Map<string, File | Folder | ModuleFunction> = new Map(); // eslint-disable-next-line no-restricted-syntax for (const [filePath, module] of modules.entries()) { let { functions } = module; // If we have a search query, skip modules that don't match. if (searchTerm !== "") { const filteredFunctions = functions.filter((f) => test(searchTerm, `${filePath}:${f.name}`), ); const functionNameMatches = filteredFunctions.length > 0; if (functionNameMatches) { functions = filteredFunctions; } else { continue; } } // We don't display any file uploaded to convex that isn't a function. if (!functions || functions.length === 0) { continue; } const pathComponents = filePath.split("/"); // NB: `i = 0` creates the root directory as the empty string. for (let i = 0; i < pathComponents.length; i++) { const parentPath = pathComponents.slice(0, i).join("/"); if (nodes.has(parentPath)) { continue; } const directory: Folder = { name: pathComponents[i - 1], identifier: parentPath, type: "folder", children: [], componentId, componentPath, }; nodes.set(parentPath, directory); } const file: File = { name: moduleDisplayName(filePath), identifier: filePath, type: "file", functions: [], componentId, componentPath, }; nodes.set(filePath, file); for (const moduleFunction of functions) { const f = processAnalyzedModuleFunction( moduleFunction, file.identifier, componentId, componentPath, ); file.functions.push(f); nodes.set(f.identifier, f); } // Sort functions by line number. file.functions.sort((a, b) => (a.lineno ?? -1) - (b.lineno ?? -1)); } // Stitch together the `children` children lists. for (const [filePath, node] of nodes.entries()) { // The root doesn't have a parent. if (filePath === ROOT_PATH) { continue; } // We only need to link files and folders into their parents. if (!(node.type === "file" || node.type === "folder")) { continue; } const pathComponents = filePath.split("/"); const parentPath = pathComponents .slice(0, pathComponents.length - 1) .join("/"); const parent = nodes.get(parentPath); if (!parent || parent.type !== "folder") { throw new Error(`Invalid parent at ${parentPath} for ${filePath}`); } parent.children.push(node); } // Sort the child lists. for (const fileOrDirectory of nodes.values()) { if (fileOrDirectory.type === "folder") { fileOrDirectory.children.sort((a, b) => { const sortKey = (f: FileOrFolder) => `${f.type === "folder" ? "0" : "1"}:${f.name}`; return sortKey(a).localeCompare(sortKey(b)); }); } } return nodes; } export function processAnalyzedModuleFunction( moduleFunction: AnalyzedModuleFunction, filePath: string, componentId: ComponentId, componentPath: string | null, ): ModuleFunction { const identifier = moduleFunction.udfType === "HttpAction" ? moduleFunction.name : `${filePath}:${moduleFunction.name}`; const name = moduleFunction.udfType !== "HttpAction" && moduleFunction.name === "default" ? moduleDisplayName(filePath) : moduleFunction.name; const nameToDisplay = moduleFunction.udfType === "HttpAction" ? name : displayName(identifier); return { name, displayName: nameToDisplay, type: "function", udfType: moduleFunction.udfType, identifier, args: moduleFunction.argsValidator, visibility: moduleFunction.visibility, lineno: moduleFunction.lineno, componentId, componentPath, file: { name: moduleDisplayName(filePath), identifier: filePath, }, }; } export function displayName( udfPath: string, componentPath?: string | null, ): string { let filePart: string = ""; let exportName: string = "default"; if (udfPath.includes(":")) { [filePart, exportName] = udfPath.split(":"); } else { filePart = udfPath; } if (filePart.endsWith(".js")) { filePart = filePart.slice(0, filePart.length - 3); } const componentSuffix = componentPath ? ` in ${componentPath}` : ""; return ( filePart + (exportName === "default" ? "" : `:${exportName}`) + componentSuffix ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server