Skip to main content
Glama

Convex MCP server

Official
by get-convex
integration-test.ts15.9 kB
import assert from "assert"; import { v4 as uuidv4 } from "uuid"; import { jsonToConvex } from "convex/values"; import { ExecuteResponseInner, defaultConsoleState, executeInner, globalConsoleState, globalDevConsole, ogConsole, setupConsole, } from "../src/executor.js"; import { Syscalls, SyscallsImpl } from "../src/syscalls.js"; import { createPackageJsonIfMissing, maybeDownloadAndLinkPackages, } from "../src/source_package.js"; import { EnvironmentVariable, FunctionName } from "../src/convex.js"; import * as fs from "node:fs"; import archiver from "archiver"; import { hashFromFile } from "../src/build_deps"; import path from "node:path"; import os from "node:os"; import { randomUUID } from "crypto"; import { Writable } from "stream"; type NewType = FunctionName; async function executeWrapper( modulePath: string, name: NewType, args: string, environmentVariables: EnvironmentVariable[], syscalls?: Syscalls, ) { createPackageJsonIfMissing(__dirname); const timeoutSecs = 300; const logLines: { level: string; messages: string[]; isTruncated: boolean; timestamp: number; }[] = []; const responseStream = new Writable({ write: (chunk, _encoding, callback) => { const chunkJson = JSON.parse(chunk); if (chunkJson.kind === "LogLine") { logLines.push(chunkJson.data); } callback(); }, }); return await globalConsoleState.run(defaultConsoleState(), async () => { const devConsole = setupConsole(responseStream); return await globalDevConsole.run(devConsole, async () => { const requestId = uuidv4(); const response = await executeInner( requestId, __dirname, modulePath, name, args, environmentVariables, timeoutSecs, syscalls ?? new SyscallsImpl( { canonicalizedPath: "", function: "" }, requestId, "", "", null, null, { requestId: randomUUID(), executionId: randomUUID(), isRoot: true, parentScheduledJob: null, parentScheduledJobComponentId: null, }, null, ), ); return { response, logLines }; }); }); } const printResponses = true; function printResponse(response: ExecuteResponseInner) { if (!printResponses) { return; } if (response.type === "success") { ogConsole.log(`SUCCESS -> ${response.udfReturn}`); } else { ogConsole.log(`ERROR -> ${response.message}`); for (const frame of response.frames ?? []) { ogConsole.log( ` ${frame.functionName ?? "[unknown]"} @ ${frame.fileName}:${ frame.lineNumber }`, ); } } } async function expectFailure(fn: () => Promise<unknown>): Promise<Error> { let error = null; try { await fn(); } catch (e: any) { error = e; } if (!error) { throw new Error(`Unexpected success`); } return error; } /** * Regression test for * `request for './transitive.js' is not yet fulfilled` * resolved in #11415 41f3fdbd270b1250cfc00e274c0a93810733cc26 * * diamond.js * | | * left.js right.js * | | * shared.js * | * transitive.js */ async function test_diamond() { const { response, logLines } = await executeWrapper( "diamond.js", "default", "", [], ); printResponse(response); if (response.type !== "success") { throw new Error(`Unexpected error`); } assert.deepEqual(response.udfReturn, "1"); assert.deepEqual(logLines, []); } /** * Test for circular dependencies. */ async function test_cyclic() { const { response, logLines } = await executeWrapper( "cyclic1.js", "default", "", [], ); printResponse(response); if (response.type !== "success") { throw new Error(`Unexpected error`); } assert.deepEqual(response.udfReturn, "1"); assert.deepEqual(logLines, []); } async function test_execute_success() { const { response, logLines } = await executeWrapper( "b.js", "default", "5", [], ); printResponse(response); if (response.type !== "success") { throw new Error(`Unexpected error`); } assert.deepEqual(response.udfReturn, "8"); assert.strictEqual(logLines.length, 1); assert.strictEqual(logLines[0].level, "LOG"); assert.deepEqual(logLines[0].messages, ["'Computing...'"]); } async function test_execute_env_var() { // Initialize and execute once. let result = await executeWrapper("d.js", "default", "", [ { name: "GLOBAL_SCOPE_VAR", value: "982" }, ]); let response = result.response; let logLines = result.logLines; printResponse(response); if (response.type !== "success") { throw new Error(`Unexpected error`); } assert.deepEqual(response.udfReturn, "982"); assert.deepEqual(logLines, []); // Call agin with same env variables. result = await executeWrapper("d.js", "default", "", [ { name: "GLOBAL_SCOPE_VAR", value: "982" }, ]); response = result.response; logLines = result.logLines; printResponse(response); if (response.type !== "success") { throw new Error(`Unexpected error`); } assert.deepEqual(response.udfReturn, "982"); assert.deepEqual(logLines, []); // Call with different env variables. Should recompile. result = await executeWrapper("d.js", "default", "", [ { name: "GLOBAL_SCOPE_VAR", value: "329" }, ]); response = result.response; logLines = result.logLines; printResponse(response); if (response.type !== "success") { throw new Error(`Unexpected error`); } assert.deepEqual(response.udfReturn, "329"); assert.deepEqual(logLines, []); } // Tests that env vars don't leak into the Node // environment. async function test_execute_env_var_sanitanization() { // Set a env variable from this outer environment. process.env.GLOBAL_SCOPE_VAR = "secret"; const error = await expectFailure(() => executeWrapper("d.js", "default", "", []), ); assert.deepEqual( error.message, "Action `default` did not return a string (returned `undefined`)", ); } async function test_execute_failure() { const { response } = await executeWrapper("b.js", "throwError", "5", []); printResponse(response); if (response.type !== "error") { throw new Error(`Unexpected success`); } assert.deepEqual(response.message, "such is life"); assert.notDeepEqual(response.frames, []); } async function test_execute_missing_module() { const error = await expectFailure(() => executeWrapper("zzz.js", "fibonacci", "very-important-arg", []), ); assert(error.message.includes("Cannot find module")); } async function test_execute_non_convex_action() { const error = await expectFailure(() => executeWrapper("a.js", "fibonacci", "very-important-arg", []), ); assert.deepEqual( error.message, "`fibonacci` wasn't registered as a Convex action in `a.js`", ); } async function test_execute_action_returns_number() { const error = await expectFailure(() => executeWrapper("b.js", "getNumber", "5", []), ); assert.deepEqual( error.message, "Action `getNumber` did not return a string (returned `8`)", ); } async function test_execute_syscall() { let syscallCount = 0; const mockSyscalls = { // eslint-disable-next-line @typescript-eslint/no-unused-vars syscall(op: string, jsonArgs: string): string { assert(false, "sync syscalls not allowed"); }, asyncSyscall(op: string, jsonArgs: string): Promise<string> { JSON.parse(jsonArgs); switch (op) { case "1.0/actions/query": syscallCount++; return Promise.resolve(JSON.stringify([7, 8])); case "1.0/actions/mutation": syscallCount++; return Promise.resolve(JSON.stringify(18)); default: throw new Error(`Unknown operation ${op}`); } }, // eslint-disable-next-line @typescript-eslint/no-unused-vars asyncJsSyscall(op: string, args: Record<string, any>): Promise<any> { throw new Error("asyncJsSyscall not allowed"); }, // eslint-disable-next-line @typescript-eslint/no-empty-function assertNoPendingSyscalls() {}, }; const { response, logLines } = await executeWrapper( "c.js", "default", "[]", [], mockSyscalls, ); printResponse(response); if (response.type !== "success") { throw new Error(`Unexpected error`); } assert.deepEqual(response.udfReturn, "[7,8]"); assert.deepEqual(logLines, []); assert.deepEqual(syscallCount, 2); } async function test_execute_invalid_syscall() { const { response } = await executeWrapper( "c.js", "default", "[]", [], new SyscallsImpl( { canonicalizedPath: "", function: "" }, "invalid-uuid", "", "", null, null, { requestId: randomUUID(), executionId: randomUUID(), isRoot: true, parentScheduledJob: null, parentScheduledJobComponentId: null, }, null, ), ); printResponse(response); if (response.type !== "error") { throw new Error(`Unexpected success`); } assert.deepEqual( response.message, "Leftover state detected. This typically happens if there are dangling " + "promises from a previous request. Did you forget to await your promises?", ); assert.notDeepEqual(response.frames, []); } async function runExample(example: string) { const { response } = await executeWrapper( "/third_party.js", example, "[]", [], ); printResponse(response); assert.equal(response.type, "success"); return jsonToConvex(JSON.parse((response as any).udfReturn)); } /* Disable until fixed (CX-3699) async function test_execute_stripe() { const checkoutUrl = (await runExample("stripeExample")) as string; assert.equal(checkoutUrl.startsWith("https://checkout.stripe.com"), true); } */ async function test_filename() { const filename = (await runExample("testFilename")) as string; assert( filename.includes( "/npm-packages/node-executor/dist/tests/integration-test.cjs", ), ); } async function test_dirname() { const dirname = (await runExample("testDirname")) as string; assert(dirname.includes("/npm-packages/node-executor/dist/tests")); } async function test_modules() { // eslint-disable-next-line @typescript-eslint/no-var-requires const module = require("module"); assert(module.builtinModules.includes("assert")); assert(module.builtinModules.includes("async_hooks")); assert(module.builtinModules.includes("process")); assert(module.builtinModules.includes("trace_events")); assert(module.isBuiltin("async_hooks")); assert(module.isBuiltin("node:async_hooks")); assert(!module.isBuiltin("sync_hooks")); assert(!module.isBuiltin("node:sync_hooks")); } // Make sure that instanceof works as expected. Used to be a big deal when // we used vm.Module, but works ok when leverage Node.js async function test_contexts() { const { response, logLines } = await executeWrapper( "contexts.js", "default", "", [], ); printResponse(response); if (response.type !== "success") { throw new Error(`Unexpected error`); } for (const [name, succeeded] of Object.entries( JSON.parse(response.udfReturn) as Record<string, boolean>, )) { assert.deepEqual(succeeded, true, name); } assert.deepEqual(logLines, []); } async function test_download() { // Write the source zip file with metadata.json and modules/ const sourceOutput = fs.createWriteStream(`${__dirname}/source.zip`); const sourceZip = archiver("zip"); const sourceStream = sourceZip.pipe(sourceOutput); sourceZip.directory(`${__dirname}/modules`, "modules"); sourceZip.file(`${__dirname}/metadata.json`, { name: "metadata.json" }); sourceZip.finalize(); await new Promise((resolve) => { sourceStream .on("finish", () => { resolve(null); }) .on("error", (err) => { throw err; }); }); const sourceHash = await hashFromFile(`${__dirname}/source.zip`); // Write the external deps zip file const externalDepsOutput = fs.createWriteStream( `${__dirname}/external_modules.zip`, ); const externalDepsZip = archiver("zip"); const externalDepsStream = externalDepsZip.pipe(externalDepsOutput); externalDepsZip.directory(`${__dirname}/external_modules`, "node_modules"); externalDepsZip.finalize(); await new Promise((resolve) => { externalDepsStream .on("finish", () => { resolve(null); }) .on("error", (err) => { throw err; }); }); const externalDepsHash = await hashFromFile( `${__dirname}/external_modules.zip`, ); const sourceHashDigest = sourceHash.digest().toString("base64url"); const sourcePackage = { uri: `file:${__dirname}/source.zip`, key: "test_modules_key", sha256: sourceHashDigest, bundled_source: { uri: `file:${__dirname}/source.zip`, key: "test_modules_key", sha256: sourceHashDigest, }, external_deps: { uri: `file:${__dirname}/external_modules.zip`, key: "test_external_deps_key", sha256: externalDepsHash.digest().toString("base64url"), }, }; const local = await maybeDownloadAndLinkPackages(sourcePackage); const sourceDir = path.join(os.tmpdir(), `source/test_modules_key`); assert.equal(local.dir, sourceDir); assert.equal(local.modules.has("a.js"), true); assert.equal(local.modules.has("someFolder/someFile.js"), true); // Non-node modules assert.equal(local.modules.has("third_party.js"), false); assert.equal(local.modules.has("d.js"), false); // We don’t include _deps in `.modules` since they can’t contain Convex functions assert.equal(local.modules.has("_deps/node/sample_node.js"), false); assert.equal(local.modules.has("_deps/sample_isolate.js"), false); // Assert external deps package exists after download const externalDepsDir = path.join( os.tmpdir(), `external_deps/test_external_deps_key`, ); assert.equal(fs.existsSync(path.join(externalDepsDir, "node_modules")), true); } async function test_logging() { let result = await executeWrapper("logging.js", "logSome", "[]", []); let logLines = result.logLines; assert.strictEqual(logLines.length, 40); assert.deepEqual(logLines[0].messages, ["'Hello'"]); result = await executeWrapper("logging.js", "logTooManyLines", "[]", []); logLines = result.logLines; assert.strictEqual(logLines.length, 257); assert.deepEqual(logLines[0].messages, ["'Hello'"]); assert( logLines[256].messages[0].includes( "Log overflow (maximum 256). Remaining log lines omitted.", ), ); result = await executeWrapper("logging.js", "logOverTotalLength", "[]", []); logLines = result.logLines; assert.strictEqual(logLines.length, 32); assert( logLines[logLines.length - 1].messages[0].includes( "Log overflow (maximum 1M characters). Remaining log lines omitted.", ), ); } (async () => { await test_diamond(); await test_contexts(); // Expected failure: our current algorithm can't do this let failed = false; try { await test_cyclic(); } catch (e) { ogConsole.log(e); failed = true; } if (!failed) { assert.fail("Expected test to fail"); } await test_execute_success(); await test_execute_env_var(); await test_execute_env_var_sanitanization(); await test_execute_failure(); await test_execute_missing_module(); await test_execute_non_convex_action(); await test_execute_action_returns_number(); await test_execute_syscall(); await test_execute_invalid_syscall(); // Disable until fixed (CX-3699) // await test_execute_stripe(); await test_filename(); await test_dirname(); await test_modules(); await test_download(); await test_logging(); })();

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