Skip to main content
Glama

Convex MCP server

Official
by get-convex
component_definitions.rs23.1 kB
use std::{ collections::{ BTreeMap, BTreeSet, }, str::FromStr, sync::Arc, }; use anyhow::Context; use common::{ bootstrap_model::components::definition::{ ComponentDefinitionMetadata, ComponentDefinitionType, SerializedComponentDefinitionMetadata, }, components::{ ComponentDefinitionPath, ComponentName, Resource, }, knobs::{ DATABASE_UDF_SYSTEM_TIMEOUT, DATABASE_UDF_USER_TIMEOUT, }, log_lines::LogLevel, runtime::{ Runtime, UnixTimestamp, }, types::{ EnvVarName, EnvVarValue, }, }; use deno_core::{ serde_v8, v8::{ self, GetPropertyNamesArgsBuilder, }, ModuleSpecifier, }; use errors::ErrorMetadata; use model::{ config::types::ModuleConfig, modules::module_versions::{ FullModuleSource, ModuleSource, }, }; use rand_chacha::ChaCha12Rng; use serde_json::Value as JsonValue; use udf::EvaluateAppDefinitionsResult; use value::{ base64, identifier::Identifier, ConvexObject, FieldName, NamespacedTableMapping, }; use super::{ AsyncOpRequest, IsolateEnvironment, }; use crate::{ concurrency_limiter::ConcurrencyPermit, environment::{ helpers::syscall_error::{ syscall_description_for_error, syscall_name_for_error, }, ModuleCodeCacheResult, }, helpers, isolate::{ Isolate, CONVEX_SCHEME, }, request_scope::RequestScope, strings, timeout::Timeout, }; pub struct AppDefinitionEvaluator { pub app_definition: ModuleConfig, pub component_definitions: BTreeMap<ComponentDefinitionPath, ModuleConfig>, pub dependency_graph: BTreeSet<(ComponentDefinitionPath, ComponentDefinitionPath)>, user_environment_variables: BTreeMap<EnvVarName, EnvVarValue>, // NOTE: includes both default_system_env_vars and system_env_var_overrides system_env_vars: BTreeMap<EnvVarName, EnvVarValue>, } impl AppDefinitionEvaluator { pub fn new( app_definition: ModuleConfig, component_definitions: BTreeMap<ComponentDefinitionPath, ModuleConfig>, dependency_graph: BTreeSet<(ComponentDefinitionPath, ComponentDefinitionPath)>, user_environment_variables: BTreeMap<EnvVarName, EnvVarValue>, system_env_vars: BTreeMap<EnvVarName, EnvVarValue>, ) -> Self { Self { app_definition, component_definitions, dependency_graph, user_environment_variables, system_env_vars, } } pub async fn evaluate<RT: Runtime>( self, client_id: String, isolate: &mut Isolate<RT>, ) -> anyhow::Result<EvaluateAppDefinitionsResult> { let mut in_progress = BTreeSet::new(); enum TraversalState { FirstVisit(ComponentDefinitionPath), SecondVisit(ComponentDefinitionPath), } let mut stack = vec![TraversalState::FirstVisit(ComponentDefinitionPath::root())]; let mut definitions = BTreeMap::new(); // Perform a post-order DFS, evaluating dependencies before their parents. while let Some(node) = stack.pop() { match node { TraversalState::FirstVisit(path) => { if !in_progress.insert(path.clone()) { anyhow::bail!(ErrorMetadata::bad_request( "CyclicImport", "Found cyclic definition dependency" )); } stack.push(TraversalState::SecondVisit(path.clone())); let start = (path.clone(), ComponentDefinitionPath::min()); let dependencies = self .dependency_graph .range(start..) .take_while(|(p, _)| p == &path) .map(|(_, c)| TraversalState::FirstVisit(c.clone())); stack.extend(dependencies); }, TraversalState::SecondVisit(path) => { let (filename, source) = if path.is_root() { ( APP_CONFIG_FILE_NAME, FullModuleSource { source: self.app_definition.source.clone(), source_map: self.app_definition.source_map.clone(), }, ) } else { let component_definition = self .component_definitions .get(&path) .context("Component definition not found")?; ( COMPONENT_CONFIG_FILE_NAME, FullModuleSource { source: component_definition.source.clone(), source_map: component_definition.source_map.clone(), }, ) }; let result = self .evaluate_definition( client_id.clone(), isolate, &path, &definitions, filename, Arc::new(source), ) .await?; in_progress.remove(&path); definitions.insert(path, result); }, } } Ok(definitions) } async fn evaluate_definition<RT: Runtime>( &self, client_id: String, isolate: &mut Isolate<RT>, path: &ComponentDefinitionPath, evaluated_components: &BTreeMap<ComponentDefinitionPath, ComponentDefinitionMetadata>, filename: &str, source: Arc<FullModuleSource>, ) -> anyhow::Result<ComponentDefinitionMetadata> { let environment_variables = if path.is_root() { let mut env_vars = self.system_env_vars.clone(); env_vars.extend(self.user_environment_variables.clone()); Some(env_vars) } else { None }; let env = DefinitionEnvironment { expected_filename: filename.to_string(), source, evaluated_definitions: evaluated_components.clone(), environment_variables, }; let (handle, state) = isolate.start_request(client_id.into(), env).await?; let mut handle_scope = isolate.handle_scope(); let v8_context = v8::Context::new(&mut handle_scope, v8::ContextOptions::default()); let mut context_scope = v8::ContextScope::new(&mut handle_scope, v8_context); let mut isolate_context = RequestScope::new(&mut context_scope, handle.clone(), state, false).await?; let handle = isolate_context.handle(); let result = { let mut v8_scope = isolate_context.scope(); let mut scope = RequestScope::<RT, DefinitionEnvironment>::enter(&mut v8_scope); let url = ModuleSpecifier::parse(&format!("{CONVEX_SCHEME}:/{filename}"))?; let module = scope.eval_module(&url).await?; let namespace = module .get_module_namespace() .to_object(&mut scope) .context("Module namespace wasn't an object?")?; let default_str = strings::default.create(&mut scope)?; if namespace.has(&mut scope, default_str.into()) != Some(true) { anyhow::bail!(ErrorMetadata::bad_request( "InvalidDefinition", "Definition file is missing a default export" )); } let default_export: v8::Local<v8::Object> = namespace .get(&mut scope, default_str.into()) .context("Failed to get default export")? .try_into() .map_err(|_| { ErrorMetadata::bad_request( "InvalidDefinition", "Default export is not an object", ) })?; let property_names = namespace .get_property_names(&mut scope, GetPropertyNamesArgsBuilder::default().build()) .context("Failed to get property names")?; if property_names.length() != 1 { anyhow::bail!(ErrorMetadata::bad_request( "InvalidDefinition", "Definition module has more than one export" )); } let export_str = strings::export.create(&mut scope)?; if default_export.has(&mut scope, export_str.into()) != Some(true) { anyhow::bail!(ErrorMetadata::bad_request( "InvalidDefinition", "Default export is missing its export function" )); } let export: v8::Local<v8::Function> = default_export .get(&mut scope, export_str.into()) .context("Failed to get export function")? .try_into() .map_err(|_| { ErrorMetadata::bad_request( "InvalidDefinition", "Export function is not a function", ) })?; let v8_result = export .call(&mut scope, default_export.into(), &[]) .context("Failed to call export function")?; // Inject the component definition path into the exported result. let result_obj: v8::Local<v8::Object> = v8_result.try_into().map_err(|_| { ErrorMetadata::bad_request("InvalidDefinition", "Export is not an object") })?; let key = strings::path.create(&mut scope)?; let path = String::from(path.clone()); let value = v8::String::new(&mut scope, &path).context("Failed to create string for path")?; anyhow::ensure!(result_obj.set(&mut scope, key.into(), value.into()) == Some(true)); let metadata: SerializedComponentDefinitionMetadata = serde_v8::from_v8(&mut scope, v8_result).map_err(|e| { let value = v8::json::stringify(&mut scope, v8_result) .map(|s| s.to_rust_string_lossy(&mut scope)) .unwrap_or_else(|| "<unknown>".to_string()); ErrorMetadata::bad_request( "InvalidDefinition", format!("Failed to deserialize {value}: {e}"), ) })?; ComponentDefinitionMetadata::try_from(metadata) .map_err(|e| ErrorMetadata::bad_request("InvalidDefinition", e.to_string()))? }; isolate_context.checkpoint(); drop(isolate_context); handle.take_termination_error(None, "evaluate_definition")??; Ok(result) } } pub struct ComponentInitializerEvaluator { pub evaluated_definitions: BTreeMap<ComponentDefinitionPath, ComponentDefinitionMetadata>, pub path: ComponentDefinitionPath, pub definition: ModuleConfig, pub args: BTreeMap<Identifier, Resource>, pub name: ComponentName, } impl ComponentInitializerEvaluator { pub fn new( evaluated_definitions: BTreeMap<ComponentDefinitionPath, ComponentDefinitionMetadata>, path: ComponentDefinitionPath, definition: ModuleConfig, args: BTreeMap<Identifier, Resource>, name: ComponentName, ) -> Self { Self { evaluated_definitions, path, definition, args, name, } } pub async fn evaluate<RT: Runtime>( self, client_id: String, isolate: &mut Isolate<RT>, ) -> anyhow::Result<BTreeMap<Identifier, Resource>> { let filename = COMPONENT_CONFIG_FILE_NAME.to_string(); let env = DefinitionEnvironment { expected_filename: filename.clone(), source: Arc::new(FullModuleSource { source: self.definition.source, source_map: self.definition.source_map, }), evaluated_definitions: self.evaluated_definitions, environment_variables: None, }; let (handle, state) = isolate.start_request(client_id.into(), env).await?; let mut handle_scope = isolate.handle_scope(); let v8_context = v8::Context::new(&mut handle_scope, v8::ContextOptions::default()); let mut context_scope = v8::ContextScope::new(&mut handle_scope, v8_context); let mut isolate_context = RequestScope::new(&mut context_scope, handle.clone(), state, true).await?; let handle = isolate_context.handle(); let result = { let mut v8_scope = isolate_context.scope(); let mut scope = RequestScope::<RT, DefinitionEnvironment>::enter(&mut v8_scope); let url = ModuleSpecifier::parse(&format!("{CONVEX_SCHEME}:/{filename}"))?; let module = scope.eval_module(&url).await?; let namespace = module .get_module_namespace() .to_object(&mut scope) .context("Module namespace wasn't an object?")?; let default_str = strings::default.create(&mut scope)?; if namespace.has(&mut scope, default_str.into()) != Some(true) { anyhow::bail!(ErrorMetadata::bad_request( "InvalidDefinition", "Definition file is missing a default export" )); } let default_export: v8::Local<v8::Object> = namespace .get(&mut scope, default_str.into()) .context("Failed to get default export")? .try_into() .map_err(|_| { ErrorMetadata::bad_request( "InvalidDefinition", "Default export is not an object", ) })?; let callback_str = strings::_onInitCallbacks.create(&mut scope)?; let callbacks: v8::Local<v8::Object> = default_export .get(&mut scope, callback_str.into()) .context("Failed to get _onInitCallbacks")? .try_into() .map_err(|_| { ErrorMetadata::bad_request( "InvalidDefinition", "_onInitCallbacks is not an object", ) })?; let name_str = v8::String::new(&mut scope, &String::from(self.name)) .context("Failed to create string for name")?; let callback: v8::Local<v8::Function> = callbacks .get(&mut scope, name_str.into()) .context("Failed to get callback")? .try_into() .map_err(|_| { ErrorMetadata::bad_request("InvalidDefinition", "Callback is not a function") })?; let mut args_obj = BTreeMap::new(); for (arg_name, value) in self.args { let Resource::Value(value) = value else { anyhow::bail!(ErrorMetadata::bad_request( "InvalidDefinition", format!("Argument {arg_name} is not a value"), )) }; args_obj.insert(FieldName::from_str(&arg_name)?, value); } let args_obj = ConvexObject::try_from(args_obj)?; let args_str = args_obj.json_serialize()?; let args_v8_str = v8::String::new(&mut scope, &args_str) .context("Failed to create string for args")?; let v8_result: v8::Local<v8::String> = callback .call(&mut scope, default_export.into(), &[args_v8_str.into()]) .context("Failed to call callback")? .try_into() .map_err(|_| { ErrorMetadata::bad_request( "InvalidDefinition", "Callback returned non-string value", ) })?; let result_str = helpers::to_rust_string(&mut scope, &v8_result)?; let result_json: JsonValue = serde_json::from_str(&result_str)?; let result_obj = ConvexObject::try_from(result_json)?; let mut result = BTreeMap::new(); for (arg_name, value) in BTreeMap::from(result_obj) { result.insert(arg_name.parse()?, Resource::Value(value)); } result }; isolate_context.checkpoint(); drop(isolate_context); handle.take_termination_error(None, "evaluate")??; Ok(result) } } const COMPONENT_CONFIG_FILE_NAME: &str = "convex.config.js"; const APP_CONFIG_FILE_NAME: &str = "convex.config.js"; struct DefinitionEnvironment { expected_filename: String, source: Arc<FullModuleSource>, evaluated_definitions: BTreeMap<ComponentDefinitionPath, ComponentDefinitionMetadata>, /// Environment variables are allowed in app but not in /// component config. environment_variables: Option<BTreeMap<EnvVarName, EnvVarValue>>, } impl<RT: Runtime> IsolateEnvironment<RT> for DefinitionEnvironment { fn trace(&mut self, _level: LogLevel, messages: Vec<String>) -> anyhow::Result<()> { tracing::warn!( "Unexpected Console access when evaluating app definition: {}", messages.join(" ") ); Ok(()) } fn rng(&mut self) -> anyhow::Result<&mut ChaCha12Rng> { anyhow::bail!(ErrorMetadata::bad_request( "NoRandomDuringDefinitionEvaluation", "Math.random unsupported when evaluating app definition" )) } fn crypto_rng(&mut self) -> anyhow::Result<super::crypto_rng::CryptoRng> { anyhow::bail!(ErrorMetadata::bad_request( "NoCryptoRngDuringDefinitionEvaluation", "Cannot use cryptographic randomness when evaluating app definition" )) } fn unix_timestamp(&mut self) -> anyhow::Result<UnixTimestamp> { anyhow::bail!(ErrorMetadata::bad_request( "NoDateDuringDefinitionEvaluation", "Date unsupported when evaluating app definition" )) } fn get_environment_variable( &mut self, name: EnvVarName, ) -> anyhow::Result<Option<EnvVarValue>> { self.environment_variables .as_ref() .map(|env_vars| env_vars.get(&name).cloned()) .context(ErrorMetadata::bad_request( "EnvironmentVariablesUnsupported", "Environment variables are only supported in the app's convex.config.ts.", )) } fn get_all_table_mappings(&mut self) -> anyhow::Result<NamespacedTableMapping> { anyhow::bail!(ErrorMetadata::bad_request( "NoTableMappingFetchDuringDefinitionEvaluation", "Getting the table mapping unsupported when evaluating app definition" )) } async fn lookup_source( &mut self, path: &str, _timeout: &mut Timeout<RT>, _permit: &mut Option<ConcurrencyPermit>, ) -> anyhow::Result<Option<(Arc<FullModuleSource>, ModuleCodeCacheResult)>> { if path == &self.expected_filename { return Ok(Some((self.source.clone(), ModuleCodeCacheResult::noop()))); } if let Some(remainder) = path.strip_prefix("_componentDeps/") { let r: anyhow::Result<_> = try { let def_path_str = String::from_utf8(base64::decode_urlsafe(remainder)?)?; ComponentDefinitionPath::from_str(&def_path_str)? }; let def_path = r.map_err(|e| ErrorMetadata::bad_request("InvalidModule", e.to_string()))?; let Some(def) = self.evaluated_definitions.get(&def_path) else { return Ok(None); }; let serialized_def = SerializedComponentDefinitionMetadata::try_from(def.clone())?; let default_name_string = match def.definition_type { ComponentDefinitionType::App => anyhow::bail!(ErrorMetadata::bad_request( "NoImportAppDuringDefinitionEvaluation", format!("App should not be imported while evaluating app definition") )), ComponentDefinitionType::ChildComponent { ref name, args: _ } => name.to_string(), }; let synthetic_module = FullModuleSource { source: ModuleSource::new(&format!( "export default {{ export: () => {{ return {} }}, componentDefinitionPath: \ \"{}\", defaultName: \"{}\"}}", serde_json::to_string(&serialized_def)?, String::from(def_path.clone()), default_name_string )), source_map: None, }; return Ok(Some(( Arc::new(synthetic_module), ModuleCodeCacheResult::noop(), ))); } anyhow::bail!(ErrorMetadata::bad_request( "NoImportModuleDuringDefinitionEvaluation", format!("Can't import {path} while evaluating app definition") )) } fn syscall(&mut self, name: &str, _args: JsonValue) -> anyhow::Result<JsonValue> { anyhow::bail!(ErrorMetadata::bad_request( "NoSyscallDuringAppDefinitionEvaluation", format!("Syscall {name} unsupported when evaluating app definition") )) } fn start_async_syscall( &mut self, name: String, _args: JsonValue, _resolver: v8::Global<v8::PromiseResolver>, ) -> anyhow::Result<()> { anyhow::bail!(ErrorMetadata::bad_request( format!("No{}DuringAppDefinition", syscall_name_for_error(&name)), format!( "{} unsupported while evaluating app definition", syscall_description_for_error(&name), ), )) } fn start_async_op( &mut self, request: AsyncOpRequest, _resolver: v8::Global<v8::PromiseResolver>, ) -> anyhow::Result<()> { anyhow::bail!(ErrorMetadata::bad_request( format!("No{}DuringAppDefinition", request.name_for_error()), format!( "{} unsupported while evaluating app definition", request.description_for_error() ), )) } fn user_timeout(&self) -> std::time::Duration { *DATABASE_UDF_USER_TIMEOUT } fn system_timeout(&self) -> std::time::Duration { *DATABASE_UDF_SYSTEM_TIMEOUT } }

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