#![forbid(unsafe_code)]
#![warn(missing_docs)]
use rmcp::handler::server::router::prompt::PromptRouter;
use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::handler::server::tool::ToolCallContext;
use rmcp::model::{
CallToolRequestParams, CallToolResponse, CallToolResult, ContentBlock, ErrorData,
Implementation, ListResourceTemplatesResult, ListResourcesResult, PaginatedRequestParams,
ReadResourceRequestParams, ReadResourceResponse, ReadResourceResult, ServerCapabilities,
ServerConfig,
};
use rmcp::service::RequestContext;
use rmcp::{RoleServer, ServerHandler, prompt_handler, tool_handler};
pub mod prompts;
pub mod resources;
pub mod tools;
pub use tools::{
EditArgs, EditOutput, GetArgs, GetOutput, ParseArgs, ParseOutput, SetArgs, SetMultidocArgs,
SetMultidocOutput, SetOutput, TOOL_NAMES, ValidateArgs, ValidateOutput, Violation, edit, get,
parse, set, set_multidoc, validate,
};
const CACHE_TTL_MS: u64 = 3_600_000;
const INSTRUCTIONS: &str = "Read and edit YAML files losslessly. \
noyalib_get reads the value at a dotted/indexed path in a file; \
noyalib_set and noyalib_set_multidoc rewrite one value while \
preserving every comment and all formatting. noyalib_parse, \
noyalib_edit and noyalib_validate do the same for YAML text given \
in the request and touch nothing on disk.";
#[derive(Debug, Clone)]
pub struct YamlServer {
tool_router: ToolRouter<Self>,
prompt_router: PromptRouter<Self>,
}
impl Default for YamlServer {
fn default() -> Self {
Self::new()
}
}
impl YamlServer {
#[must_use]
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
prompt_router: Self::prompt_router(),
}
}
}
#[tool_handler(router = self.tool_router)]
#[prompt_handler(router = self.prompt_router)]
impl ServerHandler for YamlServer {
fn get_info(&self) -> ServerConfig {
ServerConfig::new(
ServerCapabilities::builder()
.enable_tools()
.enable_prompts()
.enable_resources()
.build(),
)
.with_server_info(
Implementation::new("noyalib-mcp", env!("CARGO_PKG_VERSION"))
.with_title("Noyalib MCP")
.with_website_url(env!("CARGO_PKG_REPOSITORY")),
)
.with_instructions(INSTRUCTIONS)
}
async fn call_tool(
&self,
request: CallToolRequestParams,
context: RequestContext<RoleServer>,
) -> Result<CallToolResponse, ErrorData> {
if !self.tool_router.has_route(&request.name) {
return Ok(CallToolResult::error(vec![ContentBlock::text(format!(
"Unknown tool: {}. The tools are {}.",
request.name,
TOOL_NAMES.join(", ")
))])
.into());
}
let call = ToolCallContext::new(self, request, context);
self.tool_router.call(call).await
}
async fn list_resources(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> Result<ListResourcesResult, ErrorData> {
Ok(ListResourcesResult::with_all_items(resources::list()).with_ttl_ms(CACHE_TTL_MS))
}
async fn list_resource_templates(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> Result<ListResourceTemplatesResult, ErrorData> {
Ok(
ListResourceTemplatesResult::with_all_items(resources::templates())
.with_ttl_ms(CACHE_TTL_MS),
)
}
async fn read_resource(
&self,
request: ReadResourceRequestParams,
_context: RequestContext<RoleServer>,
) -> Result<ReadResourceResponse, ErrorData> {
let contents = resources::read(&request.uri, &self.tool_router.list_all())?;
Ok(ReadResourceResult::new(vec![contents])
.with_ttl_ms(CACHE_TTL_MS)
.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_server_describes_itself() {
let info = YamlServer::default().get_info();
assert_eq!(info.server_info.name, "noyalib-mcp");
assert_eq!(info.server_info.version, env!("CARGO_PKG_VERSION"));
assert!(info.capabilities.tools.is_some());
assert!(info.capabilities.prompts.is_some());
assert!(info.capabilities.resources.is_some());
assert!(
info.instructions
.as_deref()
.is_some_and(|i| i.contains("noyalib_get") && i.contains("noyalib_validate"))
);
}
#[test]
fn the_routers_agree_with_the_name_lists() {
let s = YamlServer::new();
for name in TOOL_NAMES {
assert!(s.tool_router.has_route(name), "{name}");
}
assert!(!s.tool_router.has_route("frobnicate"));
for name in prompts::PROMPT_NAMES {
assert!(s.prompt_router.has_route(name), "{name}");
}
}
}