Skip to main content
Glama

Convex MCP server

Official
by get-convex
format.ts14.7 kB
import { Shape } from "shapes"; import jsParser from "prettier-old-sync/parser-babel"; import standalone from "prettier-old-sync/standalone"; import { ObjectFieldType, ValidatorJSON, jsonToConvex } from "convex/values"; import { Index, SearchIndex, VectorIndex } from "convex/server"; import { format } from "date-fns"; type TableDefinition = { tableName: string; indexes: Index[]; searchIndexes: SearchIndex[]; vectorIndexes?: VectorIndex[]; stagedDbIndexes?: Index[]; stagedSearchIndexes?: SearchIndex[]; stagedVectorIndexes?: VectorIndex[]; documentType: ValidatorJSON | null; }; export type SchemaJson = { tables: TableDefinition[]; schemaValidation: boolean; }; export const prettier = (stmt: string, printWidth: number = 60) => { try { const prettierStmt: string = standalone.format(stmt, { plugins: [jsParser], printWidth, tabWidth: 2, semi: true, proseWrap: "always", parser: "babel-ts", }); return prettierStmt.trim(); } catch (e: any) { if (process.env.NEXT_PUBLIC_ENVIRONMENT === "development") { // It's safe to throw the whole error in development. throw e; } // Re-throw with only the first line of the prettier error, which describes the issue // Subsequent lines may include source code and should not be logged to our error tracking. throw new Error(e.message.split("\n")[0]); } }; export function displaySchemaFromShape({ shape, filterSystemFields = false, wrapObjects = true, }: { shape: Shape; filterSystemFields?: boolean; wrapObjects?: boolean; }): string { function objectFieldToSchema(p: { optional: boolean; shape: Shape }): string { if (p.optional) { return `v.optional(${displaySchemaFromShape({ shape: p.shape, })})`; } return `${displaySchemaFromShape({ shape: p.shape, })}`; } function fieldToSchema(p: { fieldName: string; optional: boolean; shape: Shape; }): string { return `"${p.fieldName}": ${objectFieldToSchema({ optional: p.optional, shape: p.shape, })}`; } const variant = shape; switch (variant.type) { case "Array": return `v.array(${displaySchemaFromShape({ shape: variant.shape, })})`; case "Boolean": return `v.boolean()`; case "Bytes": return `v.bytes()`; case "Float64": return `v.float64()`; case "Id": return `v.id("${variant.tableName}")`; case "Int64": return `v.int64()`; case "Map": return `v.map(${displaySchemaFromShape({ shape: variant.keyShape, })}, ${displaySchemaFromShape({ shape: variant.valueShape, })})`; case "Never": // If the developer saves an empty map, set, or array the shape of the values // is Never (we don't know it!). Convert to `v.any()` so the schema is at least functional. return `v.any()`; case "Null": return `v.null()`; case "Object": { const fieldsStr = filterSystemFields ? variant.fields // System fields like `_id` and `_creationTime` shouldn't be in schemas // (they are automatically added). .filter( (p) => p.fieldName !== "_id" && p.fieldName !== "_creationTime", ) .map((p) => fieldToSchema(p)) .join(",\n") : variant.fields.map((p) => fieldToSchema(p)).join(",\n"); return wrapObjects ? `v.object({${fieldsStr}})` : `{${fieldsStr}}`; } case "Record": { const keys = displaySchemaFromShape({ shape: variant.keyShape, }); const values = objectFieldToSchema(variant.valueShape); return `v.record(${keys}, ${values})`; } case "Set": return `v.set(${displaySchemaFromShape({ shape: variant.shape, })})`; case "String": return `v.string()`; case "Union": return `v.union(${variant.shapes // When we recurse, still consider this the root because we haven't actually // gone into nested objects yet! .map((variantShape) => displaySchemaFromShape({ shape: variantShape, filterSystemFields, }), ) .join(",")})`; case "Unknown": return `v.any()`; default: { // Enforce that the switch-case is exhaustive. variant satisfies never; throw new Error(`Unrecognized variant ${shape.type} in ${shape}`); } } } function displayDocumentType(validator: ValidatorJSON): string { switch (validator.type) { case "object": return displayObjectSchema(validator.value); case "union": return displayValidator(validator); case "any": return displayValidator(validator); default: throw new Error("Top-level validator must be an object or union."); } } export function displayObjectFieldSchema(field: ObjectFieldType): string { const validator = displayValidator(field.fieldType); return field.optional ? `v.optional(${validator})` : validator; } function displayObjectSchema(object: Record<string, ObjectFieldType>): string { return `{${Object.keys(object) .map((key) => { const valueType = displayObjectFieldSchema(object[key]); return `${key}: ${valueType}`; }) .join(",")}}`; } export function displayValidatorPretty(validator: ValidatorJSON): string { return prettier(displayValidator(validator)); } function displayValidator(validator: ValidatorJSON): string { switch (validator.type) { case "null": return `v.null()`; case "number": return `v.float64()`; case "bigint": return `v.int64()`; case "boolean": return `v.boolean()`; case "string": return `v.string()`; case "bytes": return `v.bytes()`; case "any": return `v.any()`; case "literal": switch (typeof validator.value) { case "string": return `v.literal(${JSON.stringify(validator.value)})`; case "number": return `v.literal(${validator.value})`; case "boolean": return `v.literal(${validator.value})`; case "object": return `v.literal(${jsonToConvex(validator.value)}n)`; default: { throw new Error( `Unsupported literal type: ${typeof validator.value}`, ); } } case "id": return `v.id("${validator.tableName}")`; case "array": return `v.array(${displayValidator(validator.value)})`; case "record": return `v.record(${displayValidator( validator.keys, )}, ${displayObjectFieldSchema(validator.values)})`; // Deprecated, but could be shown in History tab case "set" as any: return `v.set(${displayValidator((validator as any).value)})`; // Deprecated, but could be shown in History tab case "map" as any: return `v.map(${displayValidator( (validator as any).keys, )}, ${displayValidator((validator as any).values)})`; case "object": return `v.object(${displayObjectSchema(validator.value)})`; case "union": return `v.union(${validator.value .map((t: ValidatorJSON) => displayValidator(t)) .join(",\n")})`; default: { // Enforce that the switch-case is exhaustive. validator satisfies never; throw new Error(`Unrecognized validator variant: ${validator}`); } } } function displayIndexes(indexes: Index[], type: "staged" | "active"): string { return indexes .map((index) => { const fields = `[${index.fields // Filter out system fields that start with underscore. Ideally these are filtered out in backend (CX-3805), but for now we do it here. .filter((field) => (field.length > 0 ? field[0] !== "_" : true)) .map((field) => `"${field}"`) .join(",")}]`; return type === "staged" ? `.index("${index.indexDescriptor}", { fields: ${fields}, staged: true })` : `.index("${index.indexDescriptor}", ${fields})`; }) .join(""); } function displaySearchIndexes( searchIndexes: SearchIndex[], type: "staged" | "active", ): string { return searchIndexes .map( (searchIndex) => `.searchIndex("${searchIndex.indexDescriptor}", {searchField: "${ searchIndex.searchField }" ${appendFilterFieldsOrEmpty(searchIndex)} ${appendStagedOrEmpty(type)}})`, ) .join(""); } function appendFilterFieldsOrEmpty(index: SearchIndex | VectorIndex): string { return index.filterFields.length > 0 ? `, filterFields: [${index.filterFields .map((field) => `"${field}"`) .join(",")}]` : ""; } function appendStagedOrEmpty(type: "staged" | "active"): string { return type === "staged" ? ", staged: true" : ""; } function displayVectorIndexes( vectorIndexes: VectorIndex[], type: "staged" | "active", ): string { return vectorIndexes .map( (vectorIndex) => `.vectorIndex("${vectorIndex.indexDescriptor}" , {vectorField: "${vectorIndex.vectorField}" , dimensions: ${vectorIndex.dimensions} ${appendFilterFieldsOrEmpty(vectorIndex)} ${appendStagedOrEmpty(type)} })`, ) .join(""); } function displayTableDefinition(tableDefinition: TableDefinition): string { const documentType = displayDocumentType( tableDefinition.documentType ?? { type: "any" }, ); return `${tableDefinition.tableName}: defineTable(${documentType} )${ displayIndexes(tableDefinition.indexes, "active") + displayIndexes(tableDefinition.stagedDbIndexes ?? [], "staged") + displaySearchIndexes(tableDefinition.searchIndexes, "active") + displaySearchIndexes(tableDefinition.stagedSearchIndexes ?? [], "staged") + displayVectorIndexes(tableDefinition.vectorIndexes ?? [], "active") + displayVectorIndexes(tableDefinition.stagedVectorIndexes ?? [], "staged") }`; } export function displaySchema(schema: SchemaJson, relativePath = "") { const tables = schema.tables.map((table: any) => displayTableDefinition(table), ); return displayTableSchemas(tables, { schemaValidation: schema.schemaValidation, relativePath, }); } export function displaySchemaFromShapes( shapes: Map<string, Shape>, ): string | undefined { const tables: string[] = []; shapes.forEach((shape, table) => { if (shape.type !== "Never") { const tableShape = displaySchemaFromShape({ shape, filterSystemFields: true, wrapObjects: false, }); tables.push(`"${table}": defineTable(${tableShape})`); } }); return displayTableSchemas(tables, { schemaValidation: true, relativePath: "", }); } function displayTableSchemas( tables: string[], options: { schemaValidation: boolean; relativePath: string; }, ): string | undefined { if (!tables.length) { return undefined; } const imports = ` import { defineSchema, defineTable } from "${options.relativePath}convex/server"; import { v } from "${options.relativePath}convex/values"`; // schemaValidation is a default true flag, so we only make it false if it is explicitly set to false. // undefined => true. const schemaOptions = options.schemaValidation === false ? ", { schemaValidation: false }" : ""; return prettier(` ${imports} export default defineSchema({${tables.join(",")}}${schemaOptions});`); } // Intl.NumberFormat uses B instead of G for billion prefix, so handroll export function formatBytes(number: number): string; export function formatBytes(number: number | null): string | null; export function formatBytes(number: number | null) { const nonBreakingSpace = String.fromCharCode(160); if (number === null) { return null; } if (number === 0) { return `0${nonBreakingSpace}B`; } const k = 1024; const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const i = Math.floor(Math.log(number) / Math.log(k)); // parseFloat trick gets rid of trailing zeroes return `${parseFloat((number / k ** i).toFixed(2))}${nonBreakingSpace}${ sizes[i] }`; } const NUMBER_FORMAT = new Intl.NumberFormat(); export function formatNumber(value: number): string; export function formatNumber(value: number | null): string | null; export function formatNumber(value: number | null): string | null { if (value === null) return null; return NUMBER_FORMAT.format(value); } const NUMBER_FORMAT_COMPACT = new Intl.NumberFormat("en-US", { notation: "compact", compactDisplay: "short", }); export function formatNumberCompact(value: number | bigint): string; export function formatNumberCompact( value: number | bigint | null, ): string | null; export function formatNumberCompact( value: number | bigint | null, ): string | null { if (value === null) return null; return NUMBER_FORMAT_COMPACT.format(value); } export function msFormat(n: number): string { if (n > 1000) { let s = `${Math.floor(n / 100) / 10}`; if (!s.includes(".")) { s = `${s}.0`; } s = `${s}s`; return s; } return `${Math.floor(n)}ms`; } export function formatDateTime(date: Date): string { return format(date, "MMM dd, HH:mm:ss"); } export function formatDate(date: Date): string { return format(date, "MMMM dd, yyyy"); } export function toNumericUTC(dateString: string) { // Parsing manually the date to use UTC. const [year, month, day] = dateString.split("-"); return Date.UTC(Number(year), Number(month) - 1, Number(day)); } export const timeLabelForMinute = (value: string) => { if (!value) { return ""; } // TODO(ari): Consolidate all the time rendering logic - this is a hack // for now if (value.includes("-") || !value.includes(":")) { return value; } const [time, modifier] = value.split(" "); const [hours, minutes] = time.split(":"); const date = new Date(); const hourValue = parseInt(hours); // Handle 12-hour to 24-hour conversion let hour24 = hourValue; if (modifier === "PM" && hourValue !== 12) { hour24 = hourValue + 12; } else if (modifier === "AM" && hourValue === 12) { hour24 = 0; } date.setHours(hour24); date.setMinutes(parseInt(minutes)); const oneMinuteLater = new Date(date); oneMinuteLater.setMinutes(date.getMinutes() + 1); return `${formatTime(date)} – ${formatTime(oneMinuteLater)}`; }; const formatTime = (date: Date) => { let hours = date.getHours(); const minutes = date.getMinutes(); const ampm = hours >= 12 ? "PM" : "AM"; hours %= 12; hours = hours || 12; // the hour '0' should be '12' const strMinutes = minutes < 10 ? `0${minutes}` : minutes; return `${hours}:${strMinutes} ${ampm}`; };

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