Skip to main content
Glama

Convex MCP server

Official
by get-convex
module_versions.rs18 kB
use std::{ collections::BTreeMap, mem, ops::Deref, str::FromStr, sync::Arc, }; use async_lru::async_lru::SizedValue; use common::{ http::RoutedHttpPath, json::JsonForm as _, types::{ HttpActionRoute, RoutableMethod, UdfType, }, }; use errors::ErrorMetadata; #[cfg(any(test, feature = "testing"))] use proptest::prelude::*; use serde::{ Deserialize, Serialize, }; use sync_types::{ CanonicalizedModulePath, FunctionName, }; use value::heap_size::{ HeapSize, WithHeapSize, }; use super::function_validators::{ ArgsValidator, ReturnsValidator, }; use crate::cron_jobs::types::{ CronIdentifier, CronSpec, SerializedCronSpec, }; /// System-assigned version number for modules. pub type ModuleVersion = i64; /// User-specified JavaScript source code for a module. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct ModuleSource { source: Arc<str>, is_ascii: bool, } impl ModuleSource { pub fn new(source: &str) -> Self { Self { is_ascii: source.is_ascii(), source: source.into(), } } pub fn is_ascii(&self) -> bool { self.is_ascii } pub fn source_arc(&self) -> &Arc<str> { &self.source } } impl Deref for ModuleSource { type Target = str; fn deref(&self) -> &Self::Target { &self.source } } impl HeapSize for ModuleSource { fn heap_size(&self) -> usize { self.source.len() } } impl From<&str> for ModuleSource { fn from(value: &str) -> Self { Self::new(value) } } /// Bundler-generated source map for a `ModuleSource`. pub type SourceMap = String; #[derive(Debug, Clone)] pub struct FullModuleSource { pub source: ModuleSource, pub source_map: Option<SourceMap>, } impl SizedValue for FullModuleSource { fn size(&self) -> u64 { (self.source.heap_size() + self.source_map.heap_size()) as u64 } } #[derive(Debug, Clone, PartialEq, Eq, Default)] #[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))] pub struct AnalyzedModule { #[cfg_attr( any(test, feature = "testing"), proptest( strategy = "value::heap_size::of(prop::collection::vec(any::<AnalyzedFunction>(), \ 0..4))" ) )] pub functions: WithHeapSize<Vec<AnalyzedFunction>>, pub http_routes: Option<AnalyzedHttpRoutes>, #[cfg_attr( any(test, feature = "testing"), proptest( strategy = "prop::option::of(value::heap_size::of(prop::collection::btree_map(any::<CronIdentifier>(), \ any::<CronSpec>(), 0..4)))" ) )] pub cron_specs: Option<WithHeapSize<BTreeMap<CronIdentifier, CronSpec>>>, /// Index of the module's original source in the source map. pub source_index: Option<u32>, } impl HeapSize for AnalyzedModule { fn heap_size(&self) -> usize { self.functions.heap_size() + self.http_routes.heap_size() + self.cron_specs.heap_size() + self.source_index.heap_size() } } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SerializedAnalyzedModule { functions: Vec<SerializedAnalyzedFunction>, http_routes: Option<Vec<SerializedAnalyzedHttpRoute>>, cron_specs: Option<Vec<SerializedNamedCronSpec>>, source_mapped: Option<SerializedMappedModule>, } impl TryFrom<AnalyzedModule> for SerializedAnalyzedModule { type Error = anyhow::Error; fn try_from(m: AnalyzedModule) -> anyhow::Result<Self> { let source_mapped = m .source_index .as_ref() .map(|_source_mapped| SerializedMappedModule::try_from(m.clone())) .transpose()?; Ok(Self { functions: m .functions .into_iter() .map(TryFrom::try_from) .try_collect()?, http_routes: m .http_routes .map(|routes| routes.into_iter().map(TryFrom::try_from).try_collect()) .transpose()?, cron_specs: m .cron_specs .map(|specs| specs.into_iter().map(TryFrom::try_from).try_collect()) .transpose()?, source_mapped, }) } } impl TryFrom<SerializedAnalyzedModule> for AnalyzedModule { type Error = anyhow::Error; fn try_from(m: SerializedAnalyzedModule) -> anyhow::Result<Self> { Ok(Self { functions: m .functions .into_iter() .map(TryFrom::try_from) .try_collect()?, http_routes: m .http_routes .map(|routes| { let routes = routes.into_iter().map(TryFrom::try_from).try_collect()?; anyhow::Ok(AnalyzedHttpRoutes::new(routes)) }) .transpose()?, cron_specs: m .cron_specs .map(|specs| specs.into_iter().map(TryFrom::try_from).try_collect()) .transpose()?, source_index: m .source_mapped .and_then(|mapped_module| mapped_module.source_index), }) } } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct SerializedNamedCronSpec { identifier: String, spec: SerializedCronSpec, } impl TryFrom<(CronIdentifier, CronSpec)> for SerializedNamedCronSpec { type Error = anyhow::Error; fn try_from((identifier, spec): (CronIdentifier, CronSpec)) -> anyhow::Result<Self> { Ok(Self { identifier: identifier.to_string(), spec: SerializedCronSpec::try_from(spec)?, }) } } impl TryFrom<SerializedNamedCronSpec> for (CronIdentifier, CronSpec) { type Error = anyhow::Error; fn try_from(s: SerializedNamedCronSpec) -> anyhow::Result<Self> { Ok((s.identifier.parse()?, CronSpec::try_from(s.spec)?)) } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))] #[serde(rename_all = "camelCase", tag = "kind")] pub enum Visibility { Public, Internal, } #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] #[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))] pub struct AnalyzedSourcePosition { pub path: CanonicalizedModulePath, pub start_lineno: u32, pub start_col: u32, // Consider adding end_* in the future } impl HeapSize for AnalyzedSourcePosition { fn heap_size(&self) -> usize { self.path.as_str().heap_size() + self.start_col.heap_size() + self.start_lineno.heap_size() } } #[derive(Serialize, Deserialize)] // NOTE: serde not renamed to camelCase. struct SerializedAnalyzedSourcePosition { path: String, start_lineno: u32, start_col: u32, } impl TryFrom<AnalyzedSourcePosition> for SerializedAnalyzedSourcePosition { type Error = anyhow::Error; fn try_from(p: AnalyzedSourcePosition) -> anyhow::Result<Self> { Ok(Self { path: p.path.as_str().to_string(), start_lineno: p.start_lineno, start_col: p.start_col, }) } } impl TryFrom<SerializedAnalyzedSourcePosition> for AnalyzedSourcePosition { type Error = anyhow::Error; fn try_from(p: SerializedAnalyzedSourcePosition) -> anyhow::Result<Self> { Ok(Self { path: p.path.parse()?, start_lineno: p.start_lineno, start_col: p.start_col, }) } } pub fn invalid_function_name_error( path: &CanonicalizedModulePath, e: &anyhow::Error, ) -> ErrorMetadata { ErrorMetadata::bad_request( "InvalidFunctionName", format!("Invalid function name used in `{}`: {}", path.as_str(), e), ) } #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))] pub struct AnalyzedFunction { pub name: FunctionName, pub pos: Option<AnalyzedSourcePosition>, pub udf_type: UdfType, pub visibility: Option<Visibility>, // Leave args and returns unparsed to avoid performance overhead in common // case of reading ModuleMetadata without needing to validate the function. // JSON-serialized ArgsValidator pub args_str: Option<String>, // JSON-serialized ReturnsValidator pub returns_str: Option<String>, } impl AnalyzedFunction { pub fn new( name: FunctionName, pos: Option<AnalyzedSourcePosition>, udf_type: UdfType, visibility: Option<Visibility>, args: ArgsValidator, returns: ReturnsValidator, ) -> anyhow::Result<Self> { let args_json = args.json_serialize()?; let returns_json = returns.json_serialize()?; Ok(Self { name, pos, udf_type, visibility, args_str: Some(args_json), returns_str: Some(returns_json), }) } pub fn args(&self) -> anyhow::Result<ArgsValidator> { match &self.args_str { Some(args) => ArgsValidator::json_deserialize(args), None => Ok(ArgsValidator::Unvalidated), } } pub fn returns(&self) -> anyhow::Result<ReturnsValidator> { match &self.returns_str { Some(returns) => ReturnsValidator::json_deserialize(returns), None => Ok(ReturnsValidator::Unvalidated), } } } impl HeapSize for AnalyzedFunction { fn heap_size(&self) -> usize { // Undercount ArgsValidator for simplicity sake. self.name.heap_size() + mem::size_of::<UdfType>() + mem::size_of::<Visibility>() + mem::size_of::<ArgsValidator>() } } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SerializedAnalyzedFunction { name: String, pos: Option<SerializedAnalyzedSourcePosition>, udf_type: String, visibility: Option<Visibility>, args: Option<String>, returns: Option<String>, } impl TryFrom<AnalyzedFunction> for SerializedAnalyzedFunction { type Error = anyhow::Error; fn try_from(f: AnalyzedFunction) -> anyhow::Result<Self> { Ok(Self { name: f.name.to_string(), pos: f.pos.map(TryFrom::try_from).transpose()?, udf_type: f.udf_type.to_string(), visibility: f.visibility, args: f.args_str, returns: f.returns_str, }) } } impl TryFrom<SerializedAnalyzedFunction> for AnalyzedFunction { type Error = anyhow::Error; fn try_from(f: SerializedAnalyzedFunction) -> anyhow::Result<Self> { Ok(Self { name: FunctionName::from_str(&f.name)?, pos: f.pos.map(AnalyzedSourcePosition::try_from).transpose()?, udf_type: f.udf_type.parse()?, visibility: f.visibility, args_str: f.args, returns_str: f.returns, }) } } mod codegen_analyzed_function { use value::codegen_convex_serialization; use super::{ AnalyzedFunction, SerializedAnalyzedFunction, }; codegen_convex_serialization!(AnalyzedFunction, SerializedAnalyzedFunction); } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct SerializedHttpActionRoute { path: String, method: String, } impl TryFrom<HttpActionRoute> for SerializedHttpActionRoute { type Error = anyhow::Error; fn try_from(r: HttpActionRoute) -> anyhow::Result<Self> { Ok(Self { path: r.path, method: r.method.to_string(), }) } } impl TryFrom<SerializedHttpActionRoute> for HttpActionRoute { type Error = anyhow::Error; fn try_from(r: SerializedHttpActionRoute) -> anyhow::Result<Self> { Ok(Self { path: r.path.parse()?, method: r.method.parse()?, }) } } #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))] pub struct AnalyzedHttpRoute { pub route: HttpActionRoute, pub pos: Option<AnalyzedSourcePosition>, } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct SerializedAnalyzedHttpRoute { route: SerializedHttpActionRoute, pos: Option<SerializedAnalyzedSourcePosition>, } impl HeapSize for AnalyzedHttpRoute { fn heap_size(&self) -> usize { self.route.heap_size() + self.pos.heap_size() } } impl TryFrom<AnalyzedHttpRoute> for SerializedAnalyzedHttpRoute { type Error = anyhow::Error; fn try_from(r: AnalyzedHttpRoute) -> anyhow::Result<Self> { Ok(Self { route: SerializedHttpActionRoute::try_from(r.route)?, pos: r.pos.map(TryFrom::try_from).transpose()?, }) } } impl TryFrom<SerializedAnalyzedHttpRoute> for AnalyzedHttpRoute { type Error = anyhow::Error; fn try_from(r: SerializedAnalyzedHttpRoute) -> anyhow::Result<Self> { Ok(Self { route: HttpActionRoute::try_from(r.route)?, pos: r.pos.map(AnalyzedSourcePosition::try_from).transpose()?, }) } } #[derive(Debug, Clone, PartialEq, Eq, Default)] #[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))] pub struct AnalyzedHttpRoutes { #[cfg_attr( any(test, feature = "testing"), proptest( strategy = "value::heap_size::of(prop::collection::vec(any::<AnalyzedHttpRoute>(), \ 0..4))" ) )] routes: WithHeapSize<Vec<AnalyzedHttpRoute>>, } impl AnalyzedHttpRoutes { pub fn new(routes: Vec<AnalyzedHttpRoute>) -> Self { Self { routes: routes.into(), } } pub fn route_exact(&self, path: &str, method: RoutableMethod) -> bool { self.routes.iter().any(|AnalyzedHttpRoute { route, .. }| { if route.path.ends_with('*') { return false; } route.method == method && &route.path[..] == path }) } pub fn route_prefix( &self, path: &RoutedHttpPath, method: RoutableMethod, ) -> Option<RoutedHttpPath> { let mut longest_match: Option<RoutedHttpPath> = None; for AnalyzedHttpRoute { route, .. } in &self.routes { if route.method != method { continue; } let Some(mut prefix_path) = route.path.strip_suffix('*') else { continue; }; if prefix_path.is_empty() { prefix_path = "/"; } let Some(match_suffix) = path.strip_prefix(prefix_path) else { continue; }; let new_match = RoutedHttpPath(format!("/{match_suffix}")); if let Some(ref existing_suffix) = longest_match { // If the existing longest match has a shorter suffix, then it // matches a longer prefix. if existing_suffix.len() < match_suffix.len() { continue; } } longest_match = Some(new_match); } longest_match } } impl HeapSize for AnalyzedHttpRoutes { fn heap_size(&self) -> usize { self.routes.heap_size() } } impl IntoIterator for AnalyzedHttpRoutes { type IntoIter = Box<dyn Iterator<Item = AnalyzedHttpRoute>>; type Item = AnalyzedHttpRoute; fn into_iter(self) -> Self::IntoIter { Box::new(self.routes.into_iter()) } } impl Deref for AnalyzedHttpRoutes { type Target = [AnalyzedHttpRoute]; fn deref(&self) -> &Self::Target { &self.routes } } // TODO: consider denormalizing SerializedMappedModule into // SerializedAnalyzedModule and instead just include source information. This // requires a decent migration from Dashboard schema. // See https://github.com/get-convex/convex/pull/14382/files#r1252372646 for further discussion. #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct SerializedMappedModule { source_index: Option<u32>, functions: Vec<SerializedAnalyzedFunction>, http_routes: Option<Vec<SerializedAnalyzedHttpRoute>>, cron_specs: Option<Vec<SerializedNamedCronSpec>>, } impl TryFrom<AnalyzedModule> for SerializedMappedModule { type Error = anyhow::Error; fn try_from(m: AnalyzedModule) -> anyhow::Result<Self> { anyhow::ensure!( m.source_index.is_some(), "source_index must be set to be serializing into SerializedMappedModule" ); Ok(Self { source_index: m.source_index, functions: m .functions .into_iter() .map(TryFrom::try_from) .try_collect()?, http_routes: m .http_routes .map(|routes| routes.into_iter().map(TryFrom::try_from).try_collect()) .transpose()?, cron_specs: m .cron_specs .map(|specs| specs.into_iter().map(TryFrom::try_from).try_collect()) .transpose()?, }) } } #[cfg(test)] mod tests { use value::{ obj, ConvexObject, }; use super::AnalyzedFunction; use crate::modules::function_validators::ArgsValidator; #[test] fn test_analyzed_function_backwards_compatibility() -> anyhow::Result<()> { // Old metadata won't have `visibility` or `args` let metadata: ConvexObject = obj!( "name" => "myFunction", "lineno" => 1, "udfType" => "Query" )?; let function = AnalyzedFunction::try_from(metadata)?; // Should parse as `visibility: None`, and `args: Unvalidated`. assert_eq!(function.visibility, None); assert_eq!(function.args()?, ArgsValidator::Unvalidated); 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