Skip to main content
Glama

Convex MCP server

Official
by get-convex
metrics.rs22.8 kB
use std::{ borrow::Cow, sync::Arc, time::Duration, }; use common::{ components::ResolvedComponentFunctionPath, types::UdfType, version::Version, }; use deno_core::v8; use errors::ErrorMetadata; use fastrace::{ local::LocalSpan, Event, }; use metrics::{ log_counter, log_counter_with_labels, log_distribution, log_gauge, log_gauge_with_labels, register_convex_counter, register_convex_gauge, register_convex_histogram, CancelableTimer, IntoLabel, MetricLabel, StaticMetricLabel, StatusTimer, Timer, STATUS_LABEL, }; use prometheus::{ VMHistogram, VMHistogramVec, }; use sync_types::CanonicalizedUdfPath; use udf::{ ActionOutcome, FunctionOutcome, HttpActionOutcome, UdfOutcome, }; use crate::IsolateHeapStats; register_convex_histogram!( UDF_EXECUTE_SECONDS, "Duration of an UDF execution", &["udf_type", "npm_version", "status"] ); pub fn execute_timer(udf_type: &UdfType, npm_version: &Option<Version>) -> StatusTimer { let mut t = StatusTimer::new(&UDF_EXECUTE_SECONDS); t.add_label(udf_type.metric_label()); t.add_label(match npm_version { Some(v) => StaticMetricLabel::new("npm_version", v.to_string()), None => StaticMetricLabel::new("npm_version", "none"), }); t } register_convex_gauge!( ISOLATE_POOL_RUNNING_COUNT_INFO, "How many isolate workers are currently running work", &["pool_name", "client_id"] ); pub fn log_pool_running_count(name: &'static str, count: usize, client_id: &str) { log_gauge_with_labels( &ISOLATE_POOL_RUNNING_COUNT_INFO, count as f64, vec![ StaticMetricLabel::new("pool_name", name), MetricLabel::new("client_id", client_id), ], ); } register_convex_gauge!( ISOLATE_POOL_MAX_INFO, "How many isolate workers can be running", &["pool_name"] ); pub fn log_pool_max(name: &'static str, count: usize) { log_gauge_with_labels( &ISOLATE_POOL_MAX_INFO, count as f64, vec![StaticMetricLabel::new("pool_name", name)], ); } register_convex_gauge!( ISOLATE_POOL_ALLOCATED_COUNT_INFO, "How many isolate workers have been allocated", &["pool_name"] ); pub fn log_pool_allocated_count(name: &'static str, count: usize) { log_gauge_with_labels( &ISOLATE_POOL_ALLOCATED_COUNT_INFO, count as f64, vec![StaticMetricLabel::new("pool_name", name)], ); } pub fn is_developer_ok(outcome: &FunctionOutcome) -> bool { match &outcome { FunctionOutcome::Query(UdfOutcome { result, .. }) => result.is_ok(), FunctionOutcome::Mutation(UdfOutcome { result, .. }) => result.is_ok(), FunctionOutcome::Action(ActionOutcome { result, .. }) => result.is_ok(), FunctionOutcome::HttpAction(HttpActionOutcome { result, .. }) => match result { // The developer might hit errors after beginning to stream the response that wouldn't // be captured here udf::HttpActionResult::Streamed => true, udf::HttpActionResult::Error(_) => false, }, } } pub fn finish_execute_timer(timer: StatusTimer, outcome: &FunctionOutcome) { if is_developer_ok(outcome) { timer.finish(); } else { timer.finish_developer_error(); } } register_convex_counter!(UDF_EXECUTE_FULL_TOTAL, "UDF execution queue full count"); pub fn execute_full_error() -> ErrorMetadata { log_counter(&UDF_EXECUTE_FULL_TOTAL, 1); ErrorMetadata::overloaded( "ExecuteFullError", "Too many concurrent requests in a short period of time. Spread out your requests out \ over time or throttle them to avoid errors.", ) } register_convex_histogram!( UDF_SERVICE_REQUEST_SECONDS, "Time to service an UDF request", &["status", "udf_type"] ); pub fn service_request_timer(udf_type: &UdfType) -> StatusTimer { let mut t = StatusTimer::new(&UDF_SERVICE_REQUEST_SECONDS); t.add_label(udf_type.metric_label()); t } register_convex_histogram!( ISOLATE_SCHEDULER_STOLEN_WORKER_AGE_SECONDS, "The now - last_used_ts in seconds for the stolen worker", ); pub fn log_worker_stolen(age: Duration) { log_distribution( &ISOLATE_SCHEDULER_STOLEN_WORKER_AGE_SECONDS, age.as_secs_f64(), ); } register_convex_histogram!(UDF_QUEUE_SECONDS, "UDF queue time"); pub fn queue_timer() -> Timer<VMHistogram> { Timer::new(&UDF_QUEUE_SECONDS) } pub enum RequestStatus { Success, DeveloperError, SystemError, } pub fn finish_service_request_timer(timer: StatusTimer, status: RequestStatus) { match status { RequestStatus::Success => { timer.finish(); }, RequestStatus::DeveloperError => { timer.finish_developer_error(); }, RequestStatus::SystemError => (), }; } register_convex_histogram!( UDF_ISOLATE_BUILD_SECONDS, "Time to build isolate context", &STATUS_LABEL ); pub fn context_build_timer() -> StatusTimer { StatusTimer::new(&UDF_ISOLATE_BUILD_SECONDS) } register_convex_histogram!( UDF_ISOLATE_LOAD_USER_MODULES_SECONDS, "Time to load all user modules for a request", &["udf_type", "is_dynamic", "status"], ); pub fn eval_user_module_timer(udf_type: UdfType, is_dynamic: bool) -> StatusTimer { let mut t = StatusTimer::new(&UDF_ISOLATE_LOAD_USER_MODULES_SECONDS); t.add_label(udf_type.metric_label()); t.add_label(StaticMetricLabel::new("is_dynamic", is_dynamic.as_label())); t } register_convex_histogram!( UDF_ISOLATE_LOOKUP_SOURCE_SECONDS, "Time to load a single module's source", &["is_system", "status"], ); pub fn lookup_source_timer(is_system: bool) -> StatusTimer { let mut t = StatusTimer::new(&UDF_ISOLATE_LOOKUP_SOURCE_SECONDS); t.add_label(StaticMetricLabel::new("is_system", is_system.as_label())); t } register_convex_histogram!( UDF_ISOLATE_COMPILE_MODULE_SECONDS, "Time to compile a single module's source", &["status", "cached"], ); pub fn compile_module_timer(cached: bool) -> StatusTimer { let mut timer = StatusTimer::new(&UDF_ISOLATE_COMPILE_MODULE_SECONDS); timer.add_label(MetricLabel::new("cached", cached.as_label())); timer } register_convex_histogram!( UDF_ISOLATE_INSTANTIATE_MODULE_SECONDS, "Time to instantiate the top-level module", &["status"], ); pub fn instantiate_module_timer() -> StatusTimer { StatusTimer::new(&UDF_ISOLATE_INSTANTIATE_MODULE_SECONDS) } register_convex_histogram!( UDF_ISOLATE_EVALUATE_MODULE_SECONDS, "Time to evaluate the top-level module", &["status"], ); pub fn evaluate_module_timer() -> StatusTimer { StatusTimer::new(&UDF_ISOLATE_EVALUATE_MODULE_SECONDS) } register_convex_histogram!( UDF_ISOLATE_ARGUMENTS_BYTES, "Size of isolate arguments in bytes" ); pub fn log_argument_length(args: &str) { log_distribution(&UDF_ISOLATE_ARGUMENTS_BYTES, args.len() as f64); } register_convex_histogram!(UDF_ISOLATE_RESULT_BYTES, "Size of isolate results in bytes"); pub fn log_result_length(result: &str) { log_distribution(&UDF_ISOLATE_RESULT_BYTES, result.len() as f64); } register_convex_histogram!(UDF_OP_SECONDS, "Duration of UDF op", &["status", "op"]); pub fn op_timer(op_name: &str) -> StatusTimer { let mut t = StatusTimer::new(&UDF_OP_SECONDS); t.add_label(StaticMetricLabel::new("op", op_name.to_owned())); t } register_convex_counter!( ISOLATE_DIRECT_FUNCTION_CALL_TOTAL, "Number of calls to registered UDFs as js functions" ); fn log_direct_function_call() { log_counter(&ISOLATE_DIRECT_FUNCTION_CALL_TOTAL, 1); } pub fn log_log_line(line: &str) { // We log a console.warn line containing this link when a function is called // directly. These are potentially problematic because it looks like arg and // return values are being validated, and a new isolate is running the UDF, // but actually the plain JS function is being called. If the non-isolated, // non-validated behavior is intended, the helper function should be explicit. if line.contains("https://docs.convex.dev/production/best-practices/#use-helper-functions-to-write-shared-code") { tracing::warn!("Direct function call detected: '{line}'"); log_direct_function_call(); } } register_convex_histogram!( UDF_SYSCALL_SECONDS, "Duration of UDF syscall", &["status", "syscall"] ); pub fn syscall_timer(op_name: &str) -> StatusTimer { let mut t = StatusTimer::new(&UDF_SYSCALL_SECONDS); t.add_label(StaticMetricLabel::new("syscall", op_name.to_owned())); t } register_convex_histogram!( UDF_ASYNC_SYSCALL_SECONDS, "Duration of UDF async syscall", &["status", "syscall"] ); pub fn async_syscall_timer(op_name: &str) -> StatusTimer { let mut t = StatusTimer::new(&UDF_ASYNC_SYSCALL_SECONDS); t.add_label(StaticMetricLabel::new("syscall", op_name.to_owned())); t } register_convex_counter!( UDF_UNAWAITED_OP_TOTAL, "Count of async syscalls/ops still pending when a function resolves", &["environment"], ); pub fn log_unawaited_pending_op(count: usize, environment: &'static str) { log_counter_with_labels( &UDF_UNAWAITED_OP_TOTAL, count as u64, vec![StaticMetricLabel::new("environment", environment)], ); } register_convex_counter!( FUNCTION_LIMIT_WARNING_TOTAL, "Count of functions that exceeded some limit warning level", &["limit", "system_udf_path"] ); pub fn log_function_limit_warning( limit_name: &'static str, system_udf_path: Option<&CanonicalizedUdfPath>, ) { let labels = match system_udf_path { Some(udf_path) => vec![ StaticMetricLabel::new("limit", limit_name), StaticMetricLabel::new("system_udf_path", udf_path.to_string()), ], None => vec![ StaticMetricLabel::new("limit", limit_name), StaticMetricLabel::new("system_udf_path", "none"), ], }; log_counter_with_labels(&FUNCTION_LIMIT_WARNING_TOTAL, 1, labels); } register_convex_counter!( UDF_SOURCE_MAP_FAILURE_TOTAL, "Number of source map failures" ); pub fn log_source_map_failure(exception_message: &str, e: &anyhow::Error) { tracing::error!("Failed to extract error from {exception_message:?}: {e}"); log_counter(&UDF_SOURCE_MAP_FAILURE_TOTAL, 1); } register_convex_counter!(UDF_USER_TIMEOUT_TOTAL, "Number of UDF user timeouts"); pub fn log_user_timeout() { log_counter(&UDF_USER_TIMEOUT_TOTAL, 1); } register_convex_counter!(UDF_SYSTEM_TIMEOUT_TOTAL, "Number of UDF system timeouts"); pub fn log_system_timeout() { log_counter(&UDF_SYSTEM_TIMEOUT_TOTAL, 1); } register_convex_counter!( ARRAY_BUFFER_OOM_TOTAL, "Number of times that isolates hit the ArrayBuffer memory limit" ); pub fn log_array_buffer_oom() { log_counter(&ARRAY_BUFFER_OOM_TOTAL, 1); } register_convex_counter!( RECREATE_ISOLATE_TOTAL, "Number of times an isolate is recreated", &["reason"] ); pub fn log_recreate_isolate(reason: &'static str) { log_counter_with_labels( &RECREATE_ISOLATE_TOTAL, 1, vec![StaticMetricLabel::new("reason", reason)], ) } register_convex_counter!( ISOLATE_REQUEST_CANCELED_TOTAL, "Number of times an isolate execution have exited due to cancellation", ); pub fn log_isolate_request_cancelled() { log_counter(&ISOLATE_REQUEST_CANCELED_TOTAL, 1) } register_convex_counter!( PROMISE_HANDLER_ADDED_AFTER_REJECT_TOTAL, "Number of times a promise handler was added after rejection" ); pub fn log_promise_handler_added_after_reject() { log_counter(&PROMISE_HANDLER_ADDED_AFTER_REJECT_TOTAL, 1); } register_convex_counter!( PROMISE_REJECTED_AFTER_RESOLVED_TOTAL, "Number of times a promise was rejected after it was resolved" ); pub fn log_promise_rejected_after_resolved() { log_counter(&PROMISE_REJECTED_AFTER_RESOLVED_TOTAL, 1); } register_convex_counter!( PROMISE_RESOLVED_AFTER_RESOLVED_TOTAL, "Number of times a promise was resolved after it was resolved" ); pub fn log_promise_resolved_after_resolved() { log_counter(&PROMISE_RESOLVED_AFTER_RESOLVED_TOTAL, 1); } register_convex_histogram!(ISOLATE_USED_HEAP_SIZE_BYTES, "Isolate used heap size"); register_convex_histogram!(ISOLATE_HEAP_SIZE_LIMIT_BYTES, "Isolate heap size limit"); register_convex_histogram!(ISOLATE_AVAILABLE_SIZE_BYTES, "Isolate available size"); register_convex_histogram!(ISOLATE_HEAP_SIZE_BYTES, "Isolate heap size"); register_convex_histogram!( ISOLATE_HEAP_SIZE_EXECUTABLE_BYTES, "Isolate executable heap size " ); register_convex_histogram!(ISOLATE_EXTERNAL_MEMORY_BYTES, "Isolate external memory"); register_convex_histogram!(ISOLATE_PHYSICAL_SIZE_BYTES, "Isolate physical size"); register_convex_histogram!(ISOLATE_MALLOCED_MEMORY_BYTES, "Isolate malloc'd memory"); register_convex_histogram!( ISOLATE_PEAK_MALLOCED_MEMORY_BYTES, "Isolate peak malloc'd memory" ); register_convex_histogram!( ISOLATE_GLOBAL_HANDLES_SIZE_BYTES, "Isolate size of all global handles" ); register_convex_histogram!( ISOLATE_NATIVE_CONTEXT_TOTAL, "Isolate number of native contexts" ); register_convex_histogram!( ISOLATE_DETACHED_CONTEXT_TOTAL, "Isolate number of detached contexts" ); /// Heap statistics currently logged before building the Context and running the /// UDF, to detect leaks between UDFs. pub fn log_heap_statistics(stats: &v8::HeapStatistics) { log_distribution(&ISOLATE_USED_HEAP_SIZE_BYTES, stats.used_heap_size() as f64); log_distribution( &ISOLATE_HEAP_SIZE_LIMIT_BYTES, stats.heap_size_limit() as f64, ); log_distribution( &ISOLATE_AVAILABLE_SIZE_BYTES, stats.total_available_size() as f64, ); log_distribution(&ISOLATE_HEAP_SIZE_BYTES, stats.total_heap_size() as f64); log_distribution( &ISOLATE_HEAP_SIZE_EXECUTABLE_BYTES, stats.total_heap_size_executable() as f64, ); log_distribution( &ISOLATE_EXTERNAL_MEMORY_BYTES, stats.external_memory() as f64, ); log_distribution( &ISOLATE_PHYSICAL_SIZE_BYTES, stats.total_physical_size() as f64, ); log_distribution( &ISOLATE_MALLOCED_MEMORY_BYTES, stats.malloced_memory() as f64, ); log_distribution( &ISOLATE_PEAK_MALLOCED_MEMORY_BYTES, stats.peak_malloced_memory() as f64, ); log_distribution( &ISOLATE_GLOBAL_HANDLES_SIZE_BYTES, stats.total_global_handles_size() as f64, ); log_distribution( &ISOLATE_NATIVE_CONTEXT_TOTAL, stats.number_of_native_contexts() as f64, ); log_distribution( &ISOLATE_DETACHED_CONTEXT_TOTAL, stats.number_of_detached_contexts() as f64, ); } register_convex_gauge!( ISOLATE_TOTAL_USED_HEAP_SIZE_BYTES, "Total isolate used heap size across all isolates" ); register_convex_gauge!( ISOLATE_TOTAL_HEAP_SIZE_BYTES, "Total isolate heap size across all isolates" ); register_convex_gauge!( ISOLATE_TOTAL_HEAP_SIZE_EXECUTABLE_BYTES, "Total isolate executable heap siz across all isolates " ); register_convex_gauge!( ISOLATE_TOTAL_EXTERNAL_MEMORY_BYTES, "Total isolate external memory across all isolates" ); register_convex_gauge!( ISOLATE_TOTAL_PHYSICAL_SIZE_BYTES, "Total isolate physical size across all isolates" ); register_convex_gauge!( ISOLATE_TOTAL_MALLOCED_MEMORY_BYTES, "Total isolate malloc'd memory across all isolates" ); register_convex_gauge!( ISOLATE_TOTAL_ARRAY_BUFFER_MEMORY_BYTES, "Total isolate ArrayBuffer-allocated memory across all isolates" ); pub fn log_aggregated_heap_stats(stats: &IsolateHeapStats) { log_gauge( &ISOLATE_TOTAL_USED_HEAP_SIZE_BYTES, stats.v8_used_heap_size as f64, ); log_gauge( &ISOLATE_TOTAL_HEAP_SIZE_BYTES, stats.v8_total_heap_size as f64, ); log_gauge( &ISOLATE_TOTAL_HEAP_SIZE_EXECUTABLE_BYTES, stats.v8_total_heap_size_executable as f64, ); log_gauge( &ISOLATE_TOTAL_EXTERNAL_MEMORY_BYTES, stats.v8_external_memory_bytes as f64, ); log_gauge( &ISOLATE_TOTAL_PHYSICAL_SIZE_BYTES, stats.v8_total_physical_size as f64, ); log_gauge( &ISOLATE_TOTAL_MALLOCED_MEMORY_BYTES, stats.v8_malloced_memory as f64, ); log_gauge( &ISOLATE_TOTAL_ARRAY_BUFFER_MEMORY_BYTES, stats.array_buffer_size as f64, ); } register_convex_histogram!(UDF_FETCH_SECONDS, "Duration of UDF fetch", &STATUS_LABEL); pub fn udf_fetch_timer() -> StatusTimer { StatusTimer::new(&UDF_FETCH_SECONDS) } register_convex_histogram!(CREATE_ISOLATE_SECONDS, "Time to create a new isolate"); pub fn create_isolate_timer() -> Timer<prometheus::VMHistogram> { Timer::new(&CREATE_ISOLATE_SECONDS) } register_convex_histogram!(CREATE_CONTEXT_SECONDS, "Time to create a new V8 context"); pub fn create_context_timer() -> Timer<prometheus::VMHistogram> { Timer::new(&CREATE_CONTEXT_SECONDS) } register_convex_histogram!( CREATE_CODE_CACHE_SECONDS, "Time to create a code cache for a module", &STATUS_LABEL ); pub fn create_code_cache_timer() -> StatusTimer { StatusTimer::new(&CREATE_CODE_CACHE_SECONDS) } register_convex_histogram!( CONCURRENCY_PERMIT_ACQUIRE_SECONDS, "Time to acquire a concurrency permit. High latency indicate that isolate threads are \ oversubscribed and spend time waiting for CPU instead of waiting on async work", &STATUS_LABEL ); pub fn concurrency_permit_acquire_timer() -> CancelableTimer { CancelableTimer::new(&CONCURRENCY_PERMIT_ACQUIRE_SECONDS) } register_convex_counter!( CONCURRENCY_PERMIT_TOTAL_HOLD_TIME_SECONDS, "The total time concurrency limit was held for ", &["client_id"] ); pub fn log_concurrency_permit_used(client_id: Arc<String>, duration: Duration) { let duration_ms = duration .as_millis() .try_into() .expect("Hold duration is too long {}"); // This is fairly high cardinality but also super important metric. if duration_ms > 0 { log_counter_with_labels( &CONCURRENCY_PERMIT_TOTAL_HOLD_TIME_SECONDS, duration_ms, vec![StaticMetricLabel::new("client_id", client_id.to_string())], ); } } register_convex_counter!(UDF_FETCH_TOTAL, "Number of UDF fetches", &STATUS_LABEL); register_convex_counter!(UDF_FETCH_BYTES_TOTAL, "Number of bytes fetched in UDFs"); pub fn finish_udf_fetch_timer(t: StatusTimer, success: Result<usize, ()>) { let status_label = if let Ok(size) = success { t.finish(); log_counter(&UDF_FETCH_BYTES_TOTAL, size as u64); StaticMetricLabel::STATUS_SUCCESS } else { StaticMetricLabel::STATUS_ERROR }; log_counter_with_labels(&UDF_FETCH_TOTAL, 1, vec![status_label]); } // Analyze counters register_convex_counter!( SOURCE_MAP_MISSING_TOTAL, "Number of times source map is missing during a UDF or HTTP analysis" ); pub fn log_source_map_missing() { log_counter(&SOURCE_MAP_MISSING_TOTAL, 1); } register_convex_counter!( SOURCE_MAP_TOKEN_LOOKUP_FAILED_TOTAL, "Number of times source map exists but token lookup yields an invalid value during a UDF or \ HTTP analysis" ); pub fn log_source_map_token_lookup_failed() { log_counter(&SOURCE_MAP_TOKEN_LOOKUP_FAILED_TOTAL, 1); } register_convex_counter!( SOURCE_MAP_ORIGIN_IN_SEPARATE_MODULE_TOTAL, "Number of times the origin of a V8 Function is in a separate module during a UDF or HTTP \ analysis" ); pub fn log_source_map_origin_in_separate_module() { log_counter(&SOURCE_MAP_ORIGIN_IN_SEPARATE_MODULE_TOTAL, 1); } register_convex_histogram!(MODULE_LOAD_SECONDS, "Time to load modules", &["source"]); pub fn module_load_timer(source: &'static str) -> Timer<VMHistogramVec> { let mut timer = Timer::new_with_labels(&MODULE_LOAD_SECONDS); timer.add_label(MetricLabel::new_const("source", source)); timer } register_convex_counter!( ISOLATE_OUT_OF_MEMORY_TOTAL, "Number of times isolate ran out of memory during function execution" ); pub fn log_isolate_out_of_memory() { log_counter(&ISOLATE_OUT_OF_MEMORY_TOTAL, 1); } pub fn record_component_function_path(component_function_path: &ResolvedComponentFunctionPath) { LocalSpan::add_event(Event::new("component_function_path").with_properties(|| { let mut labels = vec![( Cow::Borrowed("udf_path"), Cow::Owned(component_function_path.udf_path.to_string()), )]; if let Some(component_path) = &component_function_path.component_path { labels.push(( Cow::Borrowed("component"), Cow::Owned(component_path.to_string()), )); } labels })); } register_convex_counter!( HTTP_ACTION_WITH_UNKNOWN_IDENTITY_TOTAL, "Number of HTTP actions that were called with an unknown identity", ); pub fn log_http_action_with_unknown_identity() { log_counter(&HTTP_ACTION_WITH_UNKNOWN_IDENTITY_TOTAL, 1); } register_convex_counter!( RUN_UDF_TOTAL, "Number of times that UDFs invoke nested UDFs", &[ "outer_type", "inner_type", "outer_observed_identity", "inner_observed_identity" ] ); pub fn log_run_udf( outer_type: UdfType, inner_type: UdfType, outer_observed_identity: bool, inner_observed_identity: bool, ) { log_counter_with_labels( &RUN_UDF_TOTAL, 1, vec![ StaticMetricLabel::new("outer_type", outer_type.to_lowercase_string()), StaticMetricLabel::new("inner_type", inner_type.to_lowercase_string()), StaticMetricLabel::new( "outer_observed_identity", outer_observed_identity.as_label(), ), StaticMetricLabel::new( "inner_observed_identity", inner_observed_identity.as_label(), ), ], ); } register_convex_counter!( COMPONENT_GET_USER_IDENTITY_TOTAL, "Number of times that components call getUserIdentity()", &["has_user_identity"] ); pub fn log_component_get_user_identity(has_user_identity: bool) { log_counter_with_labels( &COMPONENT_GET_USER_IDENTITY_TOTAL, 1, vec![StaticMetricLabel::new( "has_user_identity", has_user_identity.as_label(), )], ); } register_convex_counter!( LEGACY_POSITIONAL_ARGS_TOTAL, "Number of times that legacy positional arguments are used", ); pub fn log_legacy_positional_args() { log_counter(&LEGACY_POSITIONAL_ARGS_TOTAL, 1); }

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