Skip to main content
Glama

Convex MCP server

Official
by get-convex
FunctionEditor.tsx13.9 kB
import { BeforeMount, Editor } from "@monaco-editor/react"; import { PlayIcon } from "@radix-ui/react-icons"; import classNames from "classnames"; import { FunctionResult } from "convex/browser"; import { useQuery } from "convex/react"; // special case: too annoying to move convexServerTypes to a separate file right now import { useTheme } from "next-themes"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import udfs from "@common/udfs"; import { Uri } from "monaco-editor/esm/vs/editor/editor.api"; import { Button } from "@ui/Button"; import { Loading } from "@ui/Loading"; import { stringifyValue } from "@common/lib/stringifyValue"; import { SchemaJson, displaySchema } from "@common/lib/format"; import { useRunTestFunction } from "@common/features/functionRunner/lib/client"; import { ComponentId } from "@common/lib/useNents"; import { Result } from "@common/features/functionRunner/components/Result"; import { RunHistory, RunHistoryItem, useRunHistory, } from "@common/features/functionRunner/components/RunHistory"; import convexServerTypes from "../../../lib/generated/convexServerTypes.json"; // Used for typechecking const globals = ` import type { QueryBuilder } from "./convex/server" import type { DataModel } from "./_generated/dataModel" declare global { /** * Define a query in this Convex app's public API. * * This function will be allowed to read your Convex database and will be accessible from the client. * * @param func - The query function. It receives a {@link QueryCtx} as its first argument. * @returns The wrapped query. Include this as an \`export\` to name it and make it accessible. */ const query: QueryBuilder<DataModel, "public">; /** * Define a query that is only accessible from other Convex functions (but not from the client). * * This function will be allowed to read from your Convex database. It will not be accessible from the client. * * @param func - The query function. It receives a {@link QueryCtx} as its first argument. * @returns The wrapped query. Include this as an \`export\` to name it and make it accessible. */ const internalQuery: QueryBuilder<DataModel, "public">; const process: { env: { [key: string]: string | undefined; CONVEX_CLOUD_URL: string; CONVEX_SITE_URL: string; } } } `; // Used when bundling and at runtime const preamble = ` import { query, internalQuery } from "convex:/_system/repl/wrappers.js"; `; const generatedServer = `import { DataModelFromSchemaDefinition, GenericQueryCtx } from "../convex/server"; import { DataModel } from "./dataModel"; export type QueryCtx = GenericQueryCtx<DataModel>; `; const CONVEX_SERVER_FILES = { ...convexServerTypes, "file:///globals.d.ts": globals, "file://_generated/server.d.ts": generatedServer, }; const generatedDataModelWithoutSchema = ` import { AnyDataModel } from "../convex/server"; import type { GenericId } from "../convex/values"; /** * The type of a document stored in Convex. */ export type Doc = any; /** * An identifier for a document in Convex. * * Convex documents are uniquely identified by their \`Id\`, which is accessible * on the \`_id\` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). * * Documents can be loaded using \`db.get(id)\` in query and mutation functions. * * IDs are just strings at runtime, but this type can be used to distinguish them from other * strings when type checking. */ export type Id<TableName extends TableNames = TableNames> = GenericId<TableName>; /** * A type describing your Convex data model. * * This type includes information about what tables you have, the type of * documents stored in those tables, and the indexes defined on them. * * This type is used to parameterize methods like \`queryGeneric\` and * \`mutationGeneric\` to make them type-safe. */ export type DataModel = AnyDataModel; `; const generatedDataModelWithSchema = `import type { DataModelFromSchemaDefinition, DocumentByName, TableNamesInDataModel, SystemTableNames, } from "../convex/server"; import type { GenericId } from "../convex/values"; import schema from "../schema"; /** * The names of all of your Convex tables. */ export type TableNames = TableNamesInDataModel<DataModel>; /** * The type of a document stored in Convex. * * @typeParam TableName - A string literal type of the table name (like "users"). */ export type Doc<TableName extends TableNames> = DocumentByName< DataModel, TableName >; /** * An identifier for a document in Convex. * * Convex documents are uniquely identified by their \`Id\`, which is accessible * on the \`_id\` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). * * Documents can be loaded using \`db.get(id)\` in query and mutation functions. * * IDs are just strings at runtime, but this type can be used to distinguish them from other * strings when type checking. * * @typeParam TableName - A string literal type of the table name (like "users"). */ export type Id<TableName extends TableNames | SystemTableNames> = GenericId<TableName>; /** * A type describing your Convex data model. * * This type includes information about what tables you have, the type of * documents stored in those tables, and the indexes defined on them. * * This type is used to parameterize methods like \`queryGeneric\` and * \`mutationGeneric\` to make them type-safe. */ export type DataModel = DataModelFromSchemaDefinition<typeof schema>; `; function defaultCode(tableName: string) { return `export default query({ handler: async (ctx) => { console.log("Write and test your query function here!"); return await ctx.db.query("${tableName}").take(10); }, })`; } export function useFunctionEditor( initialTableName: string | null, componentId: ComponentId, runHistoryItem: RunHistoryItem | undefined, setRunHistoryItem: (item: RunHistoryItem) => void, ) { const { resolvedTheme: currentTheme } = useTheme(); const prefersDark = currentTheme === "dark"; const [prevInitialTable, setPrevInitialTable] = useState< string | null | undefined >(undefined); const [code, setCode] = useState<string>(); const schemas = useQuery(udfs.getSchemas.default, { componentId, }); const schema = useMemo(() => { if (schemas === undefined) { return undefined; } return schemas.active !== undefined ? displaySchema(JSON.parse(schemas.active) as SchemaJson, "../") : null; }, [schemas]); const [isInFlight, setIsInFlight] = useState(false); const [lastRequestTiming, setLastRequestTiming] = useState<{ startedAt: number; endedAt: number; }>(); useEffect(() => { if (runHistoryItem) { setResult(undefined); setLastRequestTiming(undefined); } }, [runHistoryItem]); const [monaco, setMonaco] = useState<Parameters<BeforeMount>[0]>(); // We store this in state to avoid importing the Uri class /facepalm const [monacoModelUri, setMonacoModelUri] = useState<Uri>(); if (prevInitialTable !== initialTableName) { setPrevInitialTable(initialTableName); setCode(defaultCode(initialTableName ?? "YOUR_TABLE_NAME")); } // Refresh files related to the schema useEffect(() => { if (schema === undefined || monaco === undefined) { return; } const dataModel = schema === null ? generatedDataModelWithoutSchema : generatedDataModelWithSchema; const dataModelUri = monaco.Uri.parse("file:///_generated/dataModel.d.ts"); monaco.editor.getModel(dataModelUri)?.dispose(); monaco.editor.createModel(dataModel, "typescript", dataModelUri); if (schema !== null) { const schemaUri = monaco.Uri.parse("file:///schema.ts"); const model = monaco.editor.getModel(schemaUri); model?.dispose(); monaco.editor.createModel(schema, "typescript", schemaUri); } }, [schema, monaco]); const [result, setResult] = useState<FunctionResult>(); const runTestFunction = useRunTestFunction(); const { appendRunHistory } = useRunHistory("_testQuery", componentId); const onSave = useCallback(async () => { if (monaco === undefined || monacoModelUri === undefined) { return; } let functionResult: FunctionResult | undefined; const startedAt = Date.now(); setIsInFlight(true); try { const worker = await monaco.languages.typescript.getTypeScriptWorker(); const client = await worker(monacoModelUri); const compiled = await client.getEmitOutput(monacoModelUri.toString()); functionResult = await runTestFunction( preamble + compiled.outputFiles[0].text, componentId || undefined, ); } catch (e: any) { functionResult = { success: false, errorMessage: e.message, logLines: [], }; } finally { // Wait a moment before re-enabling the button to // avoid the user accidently re-running the function. setTimeout(() => { setIsInFlight(false); }, 100); const endedAt = Date.now(); setLastRequestTiming({ startedAt, endedAt, }); setResult(functionResult); appendRunHistory({ type: "custom", startedAt, endedAt, code: code || "", }); } }, [ monaco, monacoModelUri, runTestFunction, componentId, appendRunHistory, code, ]); // So the editor has a callback ref to call. const saveActionRef = useRef(onSave); useEffect(() => { saveActionRef.current = onSave; }, [onSave]); const queryEditor = schemas === undefined || !code ? ( <Loading /> ) : ( // Setting a min-h makes sure the editor is able to properly resize when the // function tester is expanded/collapsed <div className="flex grow flex-col gap-2"> <div className="flex w-full items-center justify-between"> <h5 className="text-xs text-content-secondary">Custom Query</h5> <RunHistory functionIdentifier="_testQuery" componentId={componentId} selectItem={(item) => { item.type === "custom" && setCode(item.code); setRunHistoryItem(item); }} /> </div> <div className="h-full min-h-0 animate-fadeInFromLoading rounded-sm border" key={runHistoryItem ? stringifyValue(runHistoryItem) : ""} > <Editor path="/queryEditor" className="pt-2" options={{ automaticLayout: true, overviewRulerBorder: false, scrollBeyondLastLine: false, tabFocusMode: true, lineNumbers: "off", lineNumbersMinChars: 0, lineDecorationsWidth: 0, minimap: { enabled: false }, overviewRulerLanes: 0, theme: prefersDark ? "vs-dark" : "vs", scrollbar: { horizontalScrollbarSize: 8, verticalScrollbarSize: 8, useShadows: false, vertical: "visible", }, contextmenu: false, bracketPairColorization: { enabled: false }, guides: { bracketPairs: false, bracketPairsHorizontal: false, highlightActiveBracketPair: false, indentation: false, highlightActiveIndentation: false, }, selectionHighlight: false, occurrencesHighlight: false, renderLineHighlight: "none", }} height="100%" defaultLanguage="typescript" value={code} beforeMount={(monaco_) => { setMonaco(monaco_); monaco_.languages.typescript.typescriptDefaults.setCompilerOptions( { target: monaco_.languages.typescript.ScriptTarget.ESNext, moduleResolution: monaco_.languages.typescript.ModuleResolutionKind.NodeJs, allowNonTsExtensions: true, isolatedModules: true, strict: true, typeRoots: ["file:///convex", "file:///_generated"], }, ); for (const [fileName, content] of Object.entries( CONVEX_SERVER_FILES, )) { const uri = monaco_.Uri.parse(fileName); !monaco_.editor.getModel(uri) && monaco_.editor.createModel(content, "typescript", uri); } }} onMount={(editor, m) => { editor.setPosition({ lineNumber: 10, column: 0 }); setMonacoModelUri(editor.getModel()!.uri); const keybindings = [m.KeyMod.CtrlCmd | m.KeyCode.Enter]; editor.addAction({ id: "saveAction", label: "Save value", keybindings, run() { !isInFlight && void saveActionRef.current(); }, }); }} onChange={(value) => { value && setCode(value); }} /> </div> </div> ); return { queryEditor, customQueryResult: ( <Result result={result} loading={isInFlight} lastRequestTiming={lastRequestTiming} requestFilter={null} startCursor={0} /> ), runCustomQueryButton: ( <Button onClick={onSave} size="sm" className={classNames("items-center justify-center", "w-full")} loading={isInFlight} icon={<PlayIcon />} > Run Custom Query </Button> ), }; }

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