/// Base component machinery shared by tools, prompts, resources and
/// resource templates, mirroring fastmcp/utilities/components.py.
///|
/// FastMCP-namespaced metadata carried in a component's `_meta`.
pub(all) struct FastMCPMeta {
tags : Array[String]
version : String?
versions : Array[String]
} derive(Debug, Eq)
///|
pub fn FastMCPMeta::empty() -> FastMCPMeta {
{ tags: [], version: None, versions: [], }
}
///|
/// Extract moonmcp metadata from a component's meta object.
///
/// Handles both the current `fastmcp` namespace and the legacy `_fastmcp`
/// namespace for compatibility with older servers.
pub fn get_fastmcp_metadata(meta : Map[String, Json]?) -> FastMCPMeta {
guard meta is Some(meta) else { return FastMCPMeta::empty() }
for key in ["fastmcp", "_fastmcp"] {
if meta.get(key) is Some(Object(metadata)) {
let tags : Array[String] = match metadata.get("tags") {
Some(Array(values)) =>
values.filter_map(fn(v) {
if v is String(s) {
Some(s)
} else {
None
}
})
_ => []
}
let version : String? = match metadata.get("version") {
Some(String(v)) => Some(v)
_ => None
}
let versions : Array[String] = match metadata.get("versions") {
Some(Array(values)) =>
values.filter_map(fn(v) {
if v is String(s) {
Some(s)
} else {
None
}
})
_ => []
}
return { tags, version, versions, }
}
}
FastMCPMeta::empty()
}
///|
/// Strip moonmcp's internal bookkeeping out of component meta.
///
/// Returns `None` when nothing public remains, so components without user
/// metadata keep an absent `_meta` rather than an empty object.
pub fn public_result_meta(meta : Map[String, Json]?) -> Map[String, Json]? {
guard meta is Some(meta) else { return None }
let public = meta.copy()
if public.get("fastmcp") is Some(Object(fastmcp_meta)) {
let public_fastmcp : Map[String, Json] = Map([])
for key, value in fastmcp_meta {
if !key.has_prefix("_") {
public_fastmcp[key] = value
}
}
if public_fastmcp.is_empty() {
public.remove("fastmcp")
} else {
public["fastmcp"] = Json(public_fastmcp)
}
}
if public.is_empty() {
None
} else {
Some(public)
}
}
///|
/// Coerce a JSON scalar to a version string.
///
/// Raises `Validation` for non-scalar values or versions containing `@`
/// (used as the key delimiter).
pub fn coerce_version(value : Json?) -> String? raise @exceptions.FastMCPError {
match value {
None | Some(Null) => None
Some(String(v)) => check_version(v)
Some(Number(n, ..)) => {
let repr = n.to_string()
let version = if repr.has_suffix(".0") {
repr[:repr.length() - 2].to_owned()
} else {
repr
}
check_version(version)
}
Some(True) | Some(False) =>
raise @exceptions.Validation(
"Version must be a string or number, got bool",
)
Some(_) =>
raise @exceptions.Validation("Version must be a string or number")
}
}
///|
fn check_version(version : String) -> String? raise @exceptions.FastMCPError {
if version.contains("@") {
raise @exceptions.Validation(
"Version string cannot contain '@' (used as key delimiter): \{version}",
)
}
Some(version)
}
///|
pub extend FastMCPMeta with Debug::{to_repr}
///|
pub extend FastMCPMeta with Eq::{not_equal, equal}