use super::tools;
pub const MANIFEST: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/embedded/plugin/claude-plugin/plugin.json"
));
pub const CODEX_MANIFEST: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/embedded/plugin/codex-plugin/plugin.json"
));
pub const PAGE: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/embedded/plugin/skill/SKILL.md"
));
const PREAMBLE: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/embedded/plugin/skill/preamble.md"
));
#[derive(Debug)]
pub struct RenderError(&'static str);
impl std::fmt::Display for RenderError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "skill_generation_failed: {}", self.0)
}
}
impl std::error::Error for RenderError {}
pub fn render() -> Result<String, RenderError> {
let registry = tools();
let query = registry
.iter()
.find(|tool| tool.name == "run_query")
.ok_or(RenderError("skill_query_tool_missing"))?;
let continuation = continuation_text(
query.input_schema.as_value(),
query.output_schema.as_value(),
)?;
if PREAMBLE.matches("{{query_continuation}}").count() != 1 {
return Err(RenderError("skill_query_marker_invalid"));
}
let mut page = PREAMBLE
.trim_end()
.replace("{{query_continuation}}", &continuation);
page.push_str("\n\n| Tool | Purpose |\n| --- | --- |\n");
for tool in registry {
page.push_str("| `");
page.push_str(&cell(tool.name));
page.push_str("` | ");
page.push_str(&cell(tool.description));
page.push_str(" |\n");
}
Ok(page)
}
fn continuation_text(
input: &serde_json::Value,
output: &serde_json::Value,
) -> Result<String, RenderError> {
Ok(format!(
"For `run_query`, pass output `{}` back as input `{}` until a bounded read is complete.",
cell(&page_cursor_field(output)?),
cell(cursor_field(input)?)
))
}
fn page_cursor_field(schema: &serde_json::Value) -> Result<String, RenderError> {
let properties = schema
.get("properties")
.and_then(serde_json::Value::as_object)
.ok_or(RenderError("skill_query_cursor_invalid"))?;
let mut cursor = None;
for (name, shape) in properties {
let page = if let Some(reference) = shape.get("$ref") {
let pointer = reference
.as_str()
.and_then(|reference| reference.strip_prefix('#'))
.ok_or(RenderError("skill_query_cursor_invalid"))?;
schema
.pointer(pointer)
.ok_or(RenderError("skill_query_cursor_invalid"))?
} else {
shape
};
let Some(fields) = page
.get("properties")
.and_then(serde_json::Value::as_object)
else {
continue;
};
let Some(entries) = fields.get("entries") else {
continue;
};
if entries.get("type").and_then(serde_json::Value::as_str) != Some("array") {
return Err(RenderError("skill_query_cursor_invalid"));
}
if cursor.is_some() {
return Err(RenderError("skill_query_cursor_ambiguous"));
}
cursor = Some(format!("{name}.{}", cursor_field(page)?));
}
cursor.ok_or(RenderError("skill_query_cursor_invalid"))
}
fn cursor_field(schema: &serde_json::Value) -> Result<&str, RenderError> {
let properties = schema
.get("properties")
.and_then(serde_json::Value::as_object)
.ok_or(RenderError("skill_query_cursor_invalid"))?;
let mut fields = properties
.iter()
.filter(|(_, shape)| nullable_string(shape));
let (name, _) = fields
.next()
.ok_or(RenderError("skill_query_cursor_invalid"))?;
if fields.next().is_some() {
return Err(RenderError("skill_query_cursor_ambiguous"));
}
Ok(name)
}
fn nullable_string(shape: &serde_json::Value) -> bool {
if let Some(types) = shape.get("type").and_then(serde_json::Value::as_array) {
return types.len() == 2
&& types.iter().any(|kind| kind == "string")
&& types.iter().any(|kind| kind == "null");
}
shape
.get("anyOf")
.and_then(serde_json::Value::as_array)
.is_some_and(|choices| {
choices.len() == 2
&& choices
.iter()
.any(|choice| choice.get("type").is_some_and(|kind| kind == "string"))
&& choices
.iter()
.any(|choice| choice.get("type").is_some_and(|kind| kind == "null"))
})
}
fn cell(text: &str) -> String {
text.replace('\\', "\\\\")
.replace('`', "\\`")
.replace('|', "\\|")
.replace(['\n', '\r', '\t'], " ")
}
#[cfg(test)]
mod tests {
use std::error::Error;
use serde_json::Value;
use super::{MANIFEST, PAGE, render};
use crate::mcp::schema::tools;
type TestResult = Result<(), Box<dyn Error>>;
#[test]
fn checked_in_page_equals_the_live_schema_registry() -> TestResult {
assert_eq!(render()?, PAGE);
assert_eq!(
PAGE.lines().filter(|line| line.starts_with("| `")).count(),
tools().len()
);
Ok(())
}
#[test]
fn rendering_is_deterministic() -> TestResult {
assert_eq!(render()?, render()?);
Ok(())
}
#[test]
fn plugin_registers_the_launcher_and_optional_lys_proof() -> TestResult {
let manifest: Value = serde_json::from_str(MANIFEST)?;
let server = &manifest["mcpServers"]["haematite"];
assert_eq!(server["command"], "${CLAUDE_PLUGIN_ROOT}/scripts/mcp.sh");
assert_eq!(server["type"], "stdio");
assert_eq!(
server["env"],
serde_json::json!({
"HAEMATITE_CONFIG": "${user_config.config_file}",
"HAEMATITE_LYS_ISSUER": "${user_config.lys_issuer}",
"HAEMATITE_LYS_AGENT": "${user_config.lys_agent}",
"HAEMATITE_KEY_FILE": "${user_config.key_file}"
})
);
assert_eq!(manifest["userConfig"]["config_file"]["required"], true);
for (name, kind) in [
("lys_issuer", "string"),
("lys_agent", "string"),
("key_file", "file"),
] {
let option = &manifest["userConfig"][name];
assert_eq!(option["type"], kind);
assert_eq!(option["required"], false);
assert_eq!(option["default"], "");
}
Ok(())
}
#[test]
fn every_registered_tool_is_taught_exactly_once() -> TestResult {
let page = render()?;
for tool in tools() {
assert_eq!(page.matches(&format!("| `{}` |", tool.name)).count(), 1);
assert!(page.contains(&super::cell(tool.description)));
}
Ok(())
}
#[test]
fn pagination_names_come_from_the_schema_instead_of_prose() -> TestResult {
let input = serde_json::json!({"properties":{"resumption":{"type":["string","null"]}}});
let page = serde_json::json!({
"properties": {
"entries": {"type":"array","items":{}},
"following": {"anyOf": [{"type":"string"}, {"type":"null"}]}
}
});
for output in [
serde_json::json!({"properties":{"results":page}}),
serde_json::json!({
"properties":{"results":{"$ref":"#/$defs/ResultPage"}},
"$defs":{"ResultPage":page}
}),
] {
let text = super::continuation_text(&input, &output)?;
assert!(text.contains("output `results.following` back as input `resumption`"));
assert!(!text.contains("`next`"));
assert!(!text.contains("`continuation`"));
}
Ok(())
}
#[test]
fn missing_or_ambiguous_cursor_shapes_refuse_by_name() -> TestResult {
for (shape, code) in [
(serde_json::json!({}), "skill_query_cursor_invalid"),
(
serde_json::json!({
"properties": {
"a": {"type": ["string", "null"]},
"b": {"type": ["string", "null"]}
}
}),
"skill_query_cursor_ambiguous",
),
] {
let error = super::cursor_field(&shape)
.err()
.ok_or("invalid cursor accepted")?;
assert!(error.to_string().contains(code));
}
Ok(())
}
#[test]
fn the_bundled_page_explains_its_local_config_and_snapshot() {
assert!(PAGE.contains("bundled `serve.json`"));
assert!(PAGE.contains("no `userConfig` prompt"));
assert!(PAGE.contains("`socket` or `data_dir`"));
assert!(PAGE.contains("new bundle directory"));
assert!(PAGE.contains("`bundle_conflict`"));
assert!(PAGE.contains("pass output `rows.next_after` back as input `after`"));
}
#[test]
fn the_page_names_pass_refusals_and_connection_close_bounds() {
let page = PAGE.split_whitespace().collect::<Vec<_>>().join(" ");
assert!(page.contains("401 `mcp_sign_in_required`"));
assert!(page.contains("401 `mcp_pass_refused`"));
assert!(page.contains("503 `mcp_store_unavailable`"));
assert!(page.contains("for up to 1 s before closing"));
assert!(page.contains("without a linger read"));
for removed in [
"lys_userinfo",
"permitted_subjects",
"mcp_lys_answer_malformed",
] {
assert!(!page.contains(removed), "the page still names {removed}");
}
}
}