Skip to main content
Glama

Convex MCP server

Official
by get-convex
components.rs21.1 kB
use std::collections::BTreeSet; use common::{ bootstrap_model::{ components::ComponentState, tables::TableState, }, components::{ CanonicalizedComponentFunctionPath, ComponentId, ComponentPath, }, pause::PauseController, runtime::Runtime, testing::assert_contains, types::{ EnvironmentVariable, FunctionCaller, }, RequestId, }; use database::{ BootstrapComponentsModel, TableModel, UserFacingModel, PERFORM_BACKFILL_LABEL, }; use errors::ErrorMetadataAnyhowExt; use futures::FutureExt; use itertools::Itertools; use keybroker::Identity; use must_let::must_let; use runtime::testing::TestRuntime; use serde_json::{ json, Value as JsonValue, }; use sync_types::CanonicalizedUdfPath; use value::{ assert_obj, ConvexObject, ConvexValue, TableName, TableNamespace, }; use crate::{ deploy_config::SchemaStatus, test_helpers::ApplicationTestExt, Application, FunctionError, FunctionReturn, }; async fn run_function<RT: Runtime>( application: &Application<RT>, udf_path: CanonicalizedUdfPath, args: Vec<JsonValue>, ) -> anyhow::Result<Result<FunctionReturn, FunctionError>> { run_component_function(application, udf_path, args, ComponentPath::root()).await } async fn run_component_function<RT: Runtime>( application: &Application<RT>, udf_path: CanonicalizedUdfPath, args: Vec<JsonValue>, component: ComponentPath, ) -> anyhow::Result<Result<FunctionReturn, FunctionError>> { application .any_udf( RequestId::new(), CanonicalizedComponentFunctionPath { component, udf_path, }, args, Identity::system(), FunctionCaller::Test, ) .boxed() .await } #[convex_macro::test_runtime] async fn test_run_component_query(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; application .load_component_tests_modules("with-schema") .await?; let result = run_function(&application, "componentEntry:list".parse()?, vec![]).await??; assert_eq!(result.log_lines.iter().collect_vec().len(), 1); Ok(()) } #[convex_macro::test_runtime] async fn test_run_component_mutation(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; application .load_component_tests_modules("with-schema") .await?; let result = run_function( &application, "componentEntry:insert".parse()?, vec![json!({"channel": "random", "text": "convex is kewl"})], ) .await?; assert!(result.is_ok()); Ok(()) } #[convex_macro::test_runtime] async fn test_run_component_action(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; application .load_component_tests_modules("with-schema") .await?; let result = run_function(&application, "componentEntry:hello".parse()?, vec![]).await??; // No logs returned because only the action inside the component logs. assert_eq!(result.log_lines.iter().collect_vec().len(), 0); Ok(()) } #[convex_macro::test_runtime] async fn test_env_vars_not_accessible_in_components(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; application.load_component_tests_modules("basic").await?; let mut tx = application.begin(Identity::system()).await?; application .create_one_environment_variable( &mut tx, EnvironmentVariable { name: "NAME".parse()?, value: "emma".parse()?, }, ) .await?; application.commit_test(tx).await?; let result = run_function(&application, "componentEntry:envVarQuery".parse()?, vec![]).await??; assert_eq!(ConvexValue::Null, result.value.unpack()); let result = run_function(&application, "componentEntry:envVarAction".parse()?, vec![]).await??; assert_eq!(ConvexValue::Null, result.value.unpack()); Ok(()) } #[convex_macro::test_runtime] async fn test_system_env_vars_not_accessible_in_components(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; application.load_component_tests_modules("basic").await?; let result = run_function( &application, "componentEntry:systemEnvVarQuery".parse()?, vec![], ) .await??; assert_eq!(ConvexValue::Null, result.value.unpack()); let result = run_function( &application, "componentEntry:systemEnvVarAction".parse()?, vec![], ) .await??; assert_eq!(ConvexValue::Null, result.value.unpack()); Ok(()) } #[convex_macro::test_runtime] async fn test_system_error_propagation(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; application.load_component_tests_modules("basic").await?; // The system error from the subquery should propagate to the top-level query. let error = run_function( &application, "errors:throwSystemErrorFromQuery".parse()?, vec![], ) .await .unwrap_err(); assert_contains(&error, "I can't go for that"); // Actions throw a JS error into user space when a call to `ctx.runAction` // throws a system error, so we don't propagate them here. let result = run_function( &application, "errors:throwSystemErrorFromAction".parse()?, vec![], ) .await? .unwrap_err(); assert_contains(&result.error, "Your request couldn't be completed"); Ok(()) } #[convex_macro::test_runtime] async fn test_paginate_within_component(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; application.load_component_tests_modules("basic").await?; let err = run_function( &application, "errors:tryPaginateWithinComponent".parse()?, vec![], ) .await? .unwrap_err(); assert_contains(&err.error, "paginate() is only supported in the app"); Ok(()) } #[convex_macro::test_runtime] async fn test_delete_tables_in_component(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; application.load_component_tests_modules("mounted").await?; let mut tx = application.begin(Identity::system()).await?; let mut components_model = BootstrapComponentsModel::new(&mut tx); let (_, component_id) = components_model.must_component_path_to_ids(&component_path())?; // Create a table in a new namespace let table_namespace = TableNamespace::from(component_id); let mut user_facing_model = UserFacingModel::new(&mut tx, table_namespace); let table_name: TableName = "test".parse()?; user_facing_model .insert(table_name.clone(), assert_obj!()) .await?; application.commit_test(tx).await?; // Confirm table exists and document is present let mut tx = application.begin(Identity::system()).await?; let mut table_model = TableModel::new(&mut tx); let count = table_model.must_count(table_namespace, &table_name).await?; assert_eq!(count, 1); assert!(table_model.table_exists(table_namespace, &table_name)); // Delete the table application .delete_tables( &Identity::system(), vec![table_name.clone()], table_namespace, ) .await?; // Confirm table no longer exists let mut tx = application.begin(Identity::system()).await?; let mut table_model = TableModel::new(&mut tx); assert!(!table_model.table_exists(table_namespace, &table_name)); Ok(()) } #[convex_macro::test_runtime] async fn test_date_now_within_component(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; application.load_component_tests_modules("basic").await?; let result = run_function(&application, "componentEntry:dateNow".parse()?, vec![]) .await?? .value; must_let!(let ConvexValue::Array(dates) = result.unpack()); assert_eq!(dates.len(), 2); must_let!(let ConvexValue::Float64(parent_date) = dates[0].clone()); must_let!(let ConvexValue::Float64(child_date) = dates[1].clone()); // Today these are equal because we're using TestRuntime. // We want the guarantee that child_date <= parent_date for queries (because // the child might be cached), and child_date == parent_date for mutations. // But right now we actually have the opposite guarantee. // TODO: Fix this. assert!(child_date >= parent_date); Ok(()) } #[convex_macro::test_runtime] async fn test_math_random_within_component(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; application.load_component_tests_modules("basic").await?; let result = run_function(&application, "componentEntry:mathRandom".parse()?, vec![]) .await?? .value; must_let!(let ConvexValue::Array(randoms) = result.unpack()); assert_eq!(randoms.len(), 2); must_let!(let ConvexValue::Float64(parent_random) = randoms[0].clone()); must_let!(let ConvexValue::Float64(child_random) = randoms[1].clone()); // Ensure that a child component has a different random seed from the parent. // TODO: the child's random seed should depend on the parent's, so the // entire query can be deterministic. assert!(parent_random != child_random); Ok(()) } pub async fn unmount_component( application: &Application<TestRuntime>, ) -> anyhow::Result<ComponentId> { application.load_component_tests_modules("mounted").await?; run_component_function( application, "messages:insertMessage".parse()?, vec![example_message().into()], component_path(), ) .await??; // Unmount component application.load_component_tests_modules("empty").await?; let mut tx = application.begin(Identity::system()).await?; let mut components_model = BootstrapComponentsModel::new(&mut tx); let (_, component_id) = components_model.must_component_path_to_ids(&component_path())?; Ok(component_id) } fn example_message() -> ConvexObject { assert_obj!("channel" => "sports", "text" => "the celtics won!") } fn component_path() -> ComponentPath { ComponentPath::deserialize(Some("component")).unwrap() } fn table_name() -> TableName { "messages".parse().unwrap() } fn system_table_name() -> TableName { "_modules".parse().unwrap() } #[convex_macro::test_runtime] async fn test_unmounted_component_state(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; let component_id = unmount_component(&application).await?; let mut tx = application.begin(Identity::system()).await?; let mut components_model = BootstrapComponentsModel::new(&mut tx); let component = components_model .load_component(component_id) .await? .unwrap(); assert!(matches!(component.state, ComponentState::Unmounted)); // Remount the component application.load_component_tests_modules("mounted").await?; let mut tx = application.begin(Identity::system()).await?; let mut components_model = BootstrapComponentsModel::new(&mut tx); // Component at the same path should be remounted with the same id. let (_, new_component_id) = components_model.must_component_path_to_ids(&component_path())?; assert_eq!(component_id, new_component_id); let component = components_model .load_component(component_id) .await? .unwrap(); assert!(matches!(component.state, ComponentState::Active)); Ok(()) } #[convex_macro::test_runtime] async fn test_unmount_cannot_call_functions(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; unmount_component(&application).await?; // Calling component function after the component is unmounted should fail let result = run_component_function( &application, "messages:listMessages".parse()?, vec![assert_obj!().into()], ComponentPath::deserialize(Some("component"))?, ) .await?; assert!(result.is_err()); Ok(()) } #[convex_macro::test_runtime] async fn test_writes_to_unmounted_tables_fails(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; let component_id = unmount_component(&application).await?; let mut tx = application.begin(Identity::system()).await?; let mut user_model = UserFacingModel::new(&mut tx, TableNamespace::from(component_id)); let error = user_model .insert(table_name(), example_message()) .await .unwrap_err(); assert!(error.is_bad_request()); assert_eq!(error.short_msg(), "UnmountedComponent"); Ok(()) } #[convex_macro::test_runtime] async fn test_data_exists_in_unmounted_components(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; let component_id = unmount_component(&application).await?; let mut tx = application.begin(Identity::system()).await?; let mut table_model = TableModel::new(&mut tx); let count = table_model .must_count(component_id.into(), &table_name()) .await?; assert_eq!(count, 1); Ok(()) } #[convex_macro::test_runtime] async fn test_descendents_unmounted(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; unmount_component(&application).await?; let mut tx = application.begin(Identity::system()).await?; let mut components_model = BootstrapComponentsModel::new(&mut tx); let env_vars_child_component = ComponentPath::deserialize(Some("envVars/component"))?; let (_, component_id) = components_model.must_component_path_to_ids(&env_vars_child_component)?; let metadata = components_model .load_component(component_id) .await? .unwrap(); assert!(matches!(metadata.state, ComponentState::Unmounted)); Ok(()) } #[convex_macro::test_runtime] async fn test_delete_unmounted_component_deletes_component(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; let component_id = unmount_component(&application).await?; let system_identity = Identity::system(); let mut tx = application.begin(system_identity.clone()).await?; let component = BootstrapComponentsModel::new(&mut tx) .load_component(component_id) .await? .unwrap(); assert!(matches!(component.state, ComponentState::Unmounted)); application .delete_component(&system_identity, component_id) .await?; let mut tx = application.begin(system_identity).await?; let component = BootstrapComponentsModel::new(&mut tx) .load_component(component_id) .await?; assert!(component.is_none()); Ok(()) } #[convex_macro::test_runtime] async fn test_delete_unmounted_component_deletes_component_tables( rt: TestRuntime, ) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; let component_id = unmount_component(&application).await?; let table_namespace = TableNamespace::from(component_id); let mut tx = application.begin(Identity::system()).await?; assert!(TableModel::new(&mut tx).table_exists(table_namespace, &table_name())); application .delete_component(&Identity::system(), component_id) .await?; let mut tx = application.begin(Identity::system()).await?; assert!(!TableModel::new(&mut tx).table_exists(table_namespace, &table_name())); Ok(()) } #[convex_macro::test_runtime] async fn test_delete_unmounted_component_deletes_component_system_tables( rt: TestRuntime, ) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; let component_id = unmount_component(&application).await?; let table_namespace = TableNamespace::from(component_id); let mut tx = application.begin(Identity::system()).await?; assert!(TableModel::new(&mut tx).table_exists(table_namespace, &system_table_name())); application .delete_component(&Identity::system(), component_id) .await?; let mut tx = application.begin(Identity::system()).await?; assert!(!TableModel::new(&mut tx).table_exists(table_namespace, &system_table_name())); Ok(()) } #[convex_macro::test_runtime] async fn test_mounted_component_delete_component_errors_out(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; application.load_component_tests_modules("mounted").await?; let mut tx = application.begin(Identity::system()).await?; let (_, component_id) = BootstrapComponentsModel::new(&mut tx).must_component_path_to_ids(&component_path())?; assert!(application .delete_component(&Identity::system(), component_id) .await .is_err()); Ok(()) } #[convex_macro::test_runtime] async fn test_infinite_loop_in_component(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; application.load_component_tests_modules("basic").await?; let err = run_function(&application, "errors:tryInfiniteLoop".parse()?, vec![]) .await? .unwrap_err(); assert_contains(&err.error, "Cross component call depth limit exceeded"); Ok(()) } #[convex_macro::test_runtime] async fn test_delete_component_with_hidden_tables(rt: TestRuntime) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; application.load_component_tests_modules("mounted").await?; // insert table for import let mut tx = application.begin(Identity::system()).await?; let (_, component_id) = BootstrapComponentsModel::new(&mut tx).must_component_path_to_ids(&component_path())?; let mut table_model = TableModel::new(&mut tx); let hidden_table_name = "hiddentable".parse()?; let tablet_id_and_table_number = table_model .insert_table_for_import( TableNamespace::from(component_id), &hidden_table_name, None, &BTreeSet::new(), ) .await?; application.commit_test(tx).await?; // Ensure the hidden table exists let mut tx = application.begin(Identity::system()).await?; let mut table_model = TableModel::new(&mut tx); let table_metadata = table_model .get_table_metadata(tablet_id_and_table_number.tablet_id) .await? .into_value(); assert_eq!(table_metadata.namespace, TableNamespace::from(component_id)); assert_eq!(table_metadata.state, TableState::Hidden); // Delete the component let component_id = unmount_component(&application).await?; application .delete_component(&Identity::system(), component_id) .await?; // Ensure the hidden table is also deleted let mut tx = application.begin(Identity::system()).await?; let mut table_model = TableModel::new(&mut tx); let table_metadata = table_model .get_table_metadata(tablet_id_and_table_number.tablet_id) .await? .into_value(); assert_eq!(table_metadata.state, TableState::Deleting); Ok(()) } #[convex_macro::test_runtime] async fn test_component_status_skips_staged_index( rt: TestRuntime, pause: PauseController, ) -> anyhow::Result<()> { let application = Application::new_for_tests(&rt).await?; application.load_component_tests_modules("basic").await?; // Insert a doc into a table run_function(&application, "errors:insertDoc".parse()?, vec![]) .await? .unwrap(); // Don't let the index get backfilled before we can check its status let _hold_guard = pause.hold(PERFORM_BACKFILL_LABEL); let start_push = application .start_push_for_layout("schema_with_index") .await?; // Confirm new schema was created in the root component assert!(start_push .schema_change .schema_ids .get(&ComponentPath::root()) .unwrap() .is_some()); // Schema status should be in progress since one index is backfilling let (schema_status, _) = application .load_component_schema_status(&Identity::system(), &start_push.schema_change) .await?; let SchemaStatus::InProgress { components } = schema_status else { anyhow::bail!("Expected InProgress schema, got {:?}", schema_status); }; let component_status = components.get(&ComponentPath::root()).unwrap(); // Schema status only tracks the one non-staged index added and skips the staged // index that was added. assert_eq!(component_status.indexes_complete, 0); assert_eq!(component_status.indexes_total, 1); Ok(()) }

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