Skip to main content
Glama

Convex MCP server

Official
by get-convex
component_api.ts16.4 kB
import path from "path"; import { Context } from "../../bundler/context.js"; import { entryPoints } from "../../bundler/index.js"; import { ComponentDirectory, toComponentDefinitionPath, } from "../lib/components/definition/directoryStructure.js"; import { StartPushResponse } from "../lib/deployApi/startPush.js"; import { importPath, moduleIdentifier } from "./api.js"; import { header } from "./common.js"; import { ComponentExports, EvaluatedComponentDefinition, } from "../lib/deployApi/componentDefinition.js"; import { ComponentDefinitionPath } from "../lib/deployApi/paths.js"; import { Identifier, Reference } from "../lib/deployApi/types.js"; import { CanonicalizedModulePath } from "../lib/deployApi/paths.js"; import { AnalyzedFunction, AnalyzedModule, Visibility, } from "../lib/deployApi/modules.js"; import { parseValidator, validatorToType } from "./validator_helpers.js"; export function componentApiJs() { const lines = []; lines.push(header("Generated `api` utility.")); lines.push(` import { anyApi, componentsGeneric } from "convex/server"; /** * A utility for referencing Convex functions in your app's API. * * Usage: * \`\`\`js * const myFunctionReference = api.myModule.myFunction; * \`\`\` */ export const api = anyApi; export const internal = anyApi; export const components = componentsGeneric(); `); return lines.join("\n"); } export function rootComponentApiCJS() { const lines = []; lines.push(header("Generated `api` utility.")); lines.push(`const { anyApi } = require("convex/server");`); lines.push(`module.exports = { api: anyApi, internal: anyApi, };`); return lines.join("\n"); } export function componentApiStubDTS() { const lines = []; lines.push(header("Generated `api` utility.")); lines.push(`import type { AnyApi, AnyComponents } from "convex/server";`); lines.push(` export declare const api: AnyApi; export declare const internal: AnyApi; export declare const components: AnyComponents; `); return lines.join("\n"); } export async function componentApiDTS( ctx: Context, startPush: StartPushResponse, rootComponent: ComponentDirectory, componentDirectory: ComponentDirectory, opts: { staticApi: boolean }, ) { const definitionPath = toComponentDefinitionPath( rootComponent, componentDirectory, ); const analysis = startPush.analysis[definitionPath]; if (!analysis) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `No analysis found for component ${definitionPath} orig: ${definitionPath}\nin\n${Object.keys(startPush.analysis).toString()}`, }); } const lines = []; lines.push(header("Generated `api` utility.")); let apiLines: AsyncGenerator<string>; if (opts.staticApi) { apiLines = codegenStaticApiObjects(ctx, analysis); } else { apiLines = codegenDynamicApiObjects( ctx, componentDirectory, startPush, definitionPath, ); } for await (const line of apiLines) { lines.push(line); } lines.push(` export declare const components: {`); for (const childComponent of analysis.definition.childComponents) { const childComponentAnalysis = startPush.analysis[childComponent.path]; if (!childComponentAnalysis) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `No analysis found for child component ${childComponent.path}`, }); } for await (const line of codegenExports( ctx, childComponent.name, childComponentAnalysis, )) { lines.push(line); } } lines.push("};"); return lines.join("\n"); } async function* codegenStaticApiObjects( ctx: Context, analysis: EvaluatedComponentDefinition, ) { yield `import type { FunctionReference } from "convex/server";`; const apiTree = await buildApiTree(ctx, analysis.functions, { kind: "public", }); yield ` /** * A utility for referencing Convex functions in your app's public API. * * Usage: * \`\`\`js * const myFunctionReference = api.myModule.myFunction; * \`\`\` */`; yield `export declare const api:`; yield* codegenApiTree(ctx, apiTree); yield ";"; yield ` /** * A utility for referencing Convex functions in your app's internal API. * * Usage: * \`\`\`js * const myFunctionReference = internal.myModule.myFunction; * \`\`\` */`; const internalTree = await buildApiTree(ctx, analysis.functions, { kind: "internal", }); yield `export declare const internal:`; yield* codegenApiTree(ctx, internalTree); yield ";"; } async function* codegenDynamicApiObjects( ctx: Context, componentDirectory: ComponentDirectory, startPush: StartPushResponse, definitionPath: ComponentDefinitionPath, ) { const absModulePaths = await entryPoints(ctx, componentDirectory.path); const modulePaths = absModulePaths.map((p) => path.relative(componentDirectory.path, p), ); for (const modulePath of modulePaths) { const ident = moduleIdentifier(modulePath); const path = importPath(modulePath); yield `import type * as ${ident} from "../${path}.js";`; } yield ` import type { ApiFromModules, FilterApi, FunctionReference, } from "convex/server"; /** * A utility for referencing Convex functions in your app's API. * * Usage: * \`\`\`js * const myFunctionReference = api.myModule.myFunction; * \`\`\` */ declare const fullApi: ApiFromModules<{ `; for (const modulePath of modulePaths) { const ident = moduleIdentifier(modulePath); const path = importPath(modulePath); yield ` "${path}": typeof ${ident},`; } yield `}>;`; yield* codegenApiWithMounts(ctx, startPush, definitionPath); yield ` export declare const api: FilterApi<typeof fullApiWithMounts, FunctionReference<any, "public">>; export declare const internal: FilterApi<typeof fullApiWithMounts, FunctionReference<any, "internal">>; `; } interface ApiTree { [identifier: string]: | { type: "branch"; branch: ApiTree } | { type: "leaf"; leaf: AnalyzedFunction }; } async function buildApiTree( ctx: Context, functions: Record<CanonicalizedModulePath, AnalyzedModule>, visibility: Visibility, ): Promise<ApiTree> { const root: ApiTree = {}; for (const [modulePath, module] of Object.entries(functions)) { const p = importPath(modulePath); if (p.startsWith("_deps/")) { continue; } for (const f of module.functions) { if (f.visibility?.kind !== visibility.kind) { continue; } let current = root; for (const pathComponent of p.split("/")) { let next = current[pathComponent]; if (!next) { next = { type: "branch", branch: {} }; current[pathComponent] = next; } if (next.type === "leaf") { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `Ambiguous function name: ${f.name} in ${modulePath}`, }); } current = next.branch; } if (current[f.name]) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `Duplicate function name: ${f.name} in ${modulePath}`, }); } current[f.name] = { type: "leaf", leaf: f }; } } return root; } async function* codegenApiTree( ctx: Context, tree: ApiTree, ): AsyncGenerator<string> { yield "{"; for (const [identifier, subtree] of Object.entries(tree)) { if (subtree.type === "branch") { yield `"${identifier}":`; yield* codegenApiTree(ctx, subtree.branch); yield ","; } else { const visibility = subtree.leaf.visibility?.kind; if (!visibility) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `Function ${subtree.leaf.name} has no visibility`, }); } const ref = await codegenFunctionReference( ctx, subtree.leaf, visibility, true, ); yield `"${identifier}": ${ref},`; } } yield "}"; } async function* codegenApiWithMounts( ctx: Context, startPush: StartPushResponse, definitionPath: ComponentDefinitionPath, ): AsyncGenerator<string> { const mountTree = await buildMountTree(ctx, startPush, definitionPath, []); if (mountTree) { yield "export type Mounts = "; yield* codegenMountTree(mountTree); yield `;`; yield `// For now fullApiWithMounts is only fullApi which provides`; yield `// jump-to-definition in component client code.`; yield `// Use Mounts for the same type without the inference.`; yield "declare const fullApiWithMounts: typeof fullApi;"; } else { yield "declare const fullApiWithMounts: typeof fullApi;"; } } function* codegenMountTree(tree: MountTree): Generator<string> { yield `{`; for (const [identifier, subtree] of Object.entries(tree)) { if (typeof subtree === "string") { yield `"${identifier}": ${subtree},`; } else { yield `"${identifier}":`; yield* codegenMountTree(subtree); yield `,`; } } yield `}`; } interface MountTree { [identifier: string]: MountTree | string; } async function buildMountTree( ctx: Context, startPush: StartPushResponse, definitionPath: ComponentDefinitionPath, attributes: string[], ): Promise<MountTree | null> { // TODO make these types more precise when receiving analysis from server const analysis = startPush.analysis[definitionPath]; if (!analysis) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `No analysis found for component ${definitionPath} orig: ${definitionPath}\nin\n${Object.keys(startPush.analysis).toString()}`, }); } let current = analysis.definition.exports.branch; for (const attribute of attributes) { const componentExport = current.find( ([identifier]) => identifier === attribute, ); if (!componentExport) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `No export found for ${attribute}`, }); } const [_, node] = componentExport; if (node.type !== "branch") { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `Expected branch at ${attribute}`, }); } current = node.branch; } return buildComponentMountTree(ctx, startPush, analysis, current); } async function buildComponentMountTree( ctx: Context, startPush: StartPushResponse, analysis: EvaluatedComponentDefinition, exports: Array<[Identifier, ComponentExports]>, ): Promise<MountTree | null> { const result: MountTree = {}; let nonEmpty = false; for (const [identifier, componentExport] of exports) { if (componentExport.type === "leaf") { // If we're at a child component reference, follow it and build its export tree. if (componentExport.leaf.startsWith("_reference/childComponent/")) { const suffix = componentExport.leaf.slice( "_reference/childComponent/".length, ); const [componentName, ...attributes] = suffix.split("/"); const childComponent = analysis.definition.childComponents.find( (c) => c.name === componentName, ); if (!childComponent) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `No child component found for ${componentName}`, }); } const childTree = await buildMountTree( ctx, startPush, childComponent.path, attributes, ); if (childTree) { result[identifier] = childTree; nonEmpty = true; } } // If we're at a function reference outside the root, codegen it as a leaf. const isRoot = analysis.definition.definitionType.type === "app"; if (!isRoot && componentExport.leaf.startsWith("_reference/function/")) { const leaf = await resolveFunctionReference( ctx, analysis, componentExport.leaf, "public", ); result[identifier] = leaf; nonEmpty = true; } } else { const subTree = await buildComponentMountTree( ctx, startPush, analysis, componentExport.branch, ); if (subTree) { result[identifier] = subTree; nonEmpty = true; } } } return nonEmpty ? result : null; } async function* codegenExports( ctx: Context, name: Identifier, analysis: EvaluatedComponentDefinition, ): AsyncGenerator<string> { yield `${name}: {`; for (const [name, componentExport] of analysis.definition.exports.branch) { yield `${name}:`; yield* codegenExport(ctx, analysis, componentExport); yield ","; } yield "},"; } async function* codegenExport( ctx: Context, analysis: EvaluatedComponentDefinition, componentExport: ComponentExports, ): AsyncGenerator<string> { if (componentExport.type === "leaf") { yield await resolveFunctionReference( ctx, analysis, componentExport.leaf, "internal", ); } else if (componentExport.type === "branch") { yield "{"; for (const [name, childExport] of componentExport.branch) { yield `${name}:`; yield* codegenExport(ctx, analysis, childExport); yield ","; } yield "}"; } } export async function resolveFunctionReference( ctx: Context, analysis: EvaluatedComponentDefinition, reference: Reference, visibility: "public" | "internal", ) { if (!reference.startsWith("_reference/function/")) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `Invalid function reference: ${reference}`, }); } const udfPath = reference.slice("_reference/function/".length); const [modulePath, functionName] = udfPath.split(":"); const canonicalizedModulePath = canonicalizeModulePath(modulePath); const analyzedModule = analysis.functions[canonicalizedModulePath]; if (!analyzedModule) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `Module not found: ${modulePath}`, }); } const analyzedFunction = analyzedModule.functions.find( (f) => f.name === functionName, ); if (!analyzedFunction) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `Function not found: ${functionName}`, }); } return await codegenFunctionReference( ctx, analyzedFunction, visibility, false, ); } async function codegenFunctionReference( ctx: Context, analyzedFunction: AnalyzedFunction, visibility: "public" | "internal", useIdType: boolean, ): Promise<string> { // The server sends down `udfType` capitalized. const udfType = analyzedFunction.udfType.toLowerCase(); let argsType = "any"; try { const argsValidator = parseValidator(analyzedFunction.args); if (argsValidator) { if (argsValidator.type === "object" || argsValidator.type === "any") { argsType = validatorToType(argsValidator, useIdType); } else { // eslint-disable-next-line no-restricted-syntax throw new Error( `Unexpected argument validator type: ${argsValidator.type}`, ); } } } catch (e) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `Invalid function args: ${analyzedFunction.args}`, errForSentry: e, }); } let returnsType = "any"; try { const returnsValidator = parseValidator(analyzedFunction.returns); if (returnsValidator) { returnsType = validatorToType(returnsValidator, useIdType); } } catch (e) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `Invalid function returns: ${analyzedFunction.returns}`, errForSentry: e, }); } return `FunctionReference<"${udfType}", "${visibility}", ${argsType}, ${returnsType}>`; } function canonicalizeModulePath(modulePath: string): CanonicalizedModulePath { if (!modulePath.endsWith(".js")) { return modulePath + ".js"; } return modulePath; }

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