#[cfg(test)]
mod tests;
use std::sync::{Arc, LazyLock};
use rmcp::model::{JsonObject, Tool};
use serde_json::Value;
pub(crate) const LIST_PROMPTS: &str = "list_prompts";
pub(crate) const RUN_PROMPT: &str = "run_prompt";
pub(crate) const CHECK_RUN: &str = "check_run";
pub(crate) const NEED_PROMPT: &str = "need_prompt";
macro_rules! capability_register {
() => {
"State the capability the way a tool author would document it: an imperative phrase naming the operation and what it acts on, with no entity names, task specifics, or conversational framing. Good: \"Build a stakeholder position report for one entity.\" Bad: \"I need to know what Herb Sutter has said about ABI stability.\""
};
}
const CAPABILITY_REGISTER: &str = capability_register!();
macro_rules! prompt_value {
() => {
"A prompt's value is a finished artifact written for the user to read, so pass it through as it stands rather than restating it."
};
}
pub(crate) use prompt_value;
const LIST_PROMPTS_DESCRIPTION: &str = "Names the PromptForge prompts this server can run. Each entry carries the prompt's name, its description, and any problem that currently stops it running. The listing is read from the catalog as it stands, so a prompt written or edited since this conversation began is already in it. run_prompt takes a name from this listing.";
const RUN_PROMPT_DESCRIPTION: &str = concat!(
"Runs the named PromptForge prompt and returns what it produced. This server executes prompts the caller names; the prompt argument is that name, as list_prompts reports it. ",
prompt_value!()
);
const CHECK_RUN_DESCRIPTION: &str = concat!(
"Collects a PromptForge run that outlived the call which started it. Takes the run id from a result whose status was running, and reports that run's status now, with its value once it has finished. ",
prompt_value!()
);
const NEED_PROMPT_DESCRIPTION: &str = concat!(
"Resolves a described PromptForge prompt to the names of the closest prompts, up to three, best first. It is for a caller who was given a prompt by description rather than by name; it returns names and runs nothing, and run_prompt takes one of them. ",
capability_register!()
);
#[must_use]
pub(crate) fn tool_definitions() -> Vec<Tool> {
BUILT_IN_DEFINITIONS
.iter()
.filter(|built_in| built_in.tool.published())
.map(|built_in| {
Tool::new(
built_in.tool.name(),
built_in.tool.description(),
Arc::clone(&built_in.input_schema),
)
})
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BuiltInTool {
ListPrompts,
RunPrompt,
NeedPrompt,
CheckRun,
}
impl BuiltInTool {
pub(crate) const ALL: [BuiltInTool; 4] = [
BuiltInTool::ListPrompts,
BuiltInTool::RunPrompt,
BuiltInTool::NeedPrompt,
BuiltInTool::CheckRun,
];
pub(crate) fn name(self) -> &'static str {
match self {
BuiltInTool::ListPrompts => LIST_PROMPTS,
BuiltInTool::RunPrompt => RUN_PROMPT,
BuiltInTool::NeedPrompt => NEED_PROMPT,
BuiltInTool::CheckRun => CHECK_RUN,
}
}
fn description(self) -> &'static str {
match self {
BuiltInTool::ListPrompts => LIST_PROMPTS_DESCRIPTION,
BuiltInTool::RunPrompt => RUN_PROMPT_DESCRIPTION,
BuiltInTool::NeedPrompt => NEED_PROMPT_DESCRIPTION,
BuiltInTool::CheckRun => CHECK_RUN_DESCRIPTION,
}
}
pub(crate) fn published(self) -> bool {
match self {
BuiltInTool::ListPrompts | BuiltInTool::RunPrompt | BuiltInTool::CheckRun => true,
BuiltInTool::NeedPrompt => cfg!(feature = "picker"),
}
}
pub(crate) fn from_name(name: &str) -> Option<BuiltInTool> {
BuiltInTool::ALL
.into_iter()
.find(|tool| tool.name() == name)
}
fn build_schema(self) -> Arc<JsonObject> {
match self {
BuiltInTool::ListPrompts => schema(
&[(
"cursor",
Some(
"A pagination cursor from a previous listing's next_cursor, to read the page after it. Omitting it reads the first page.",
),
)],
&[],
),
BuiltInTool::RunPrompt => schema(
&[
(
"prompt",
Some("The exact name of the prompt to run, as list_prompts reports it."),
),
(
"args",
Some(
"The prompt's input, as one raw string. Omitting it passes the empty string.",
),
),
],
&["prompt"],
),
BuiltInTool::NeedPrompt => schema(
&[("capability", Some(CAPABILITY_REGISTER))],
&["capability"],
),
BuiltInTool::CheckRun => schema(
&[(
"run_id",
Some("The run id from an earlier result whose status was running."),
)],
&["run_id"],
),
}
}
}
struct BuiltIn {
tool: BuiltInTool,
input_schema: Arc<JsonObject>,
}
static BUILT_IN_DEFINITIONS: LazyLock<[BuiltIn; 4]> = LazyLock::new(|| {
BuiltInTool::ALL.map(|tool| BuiltIn {
tool,
input_schema: tool.build_schema(),
})
});
#[cfg(test)]
pub(crate) fn publishes_built_in(name: &str) -> bool {
BuiltInTool::from_name(name).is_some_and(BuiltInTool::published)
}
pub(crate) fn reserved_names() -> impl Iterator<Item = &'static str> {
BuiltInTool::ALL.into_iter().map(BuiltInTool::name)
}
fn schema(properties: &[(&str, Option<&str>)], required: &[&str]) -> Arc<JsonObject> {
let mut declared = JsonObject::new();
for &(name, description) in properties {
let mut property = JsonObject::new();
property.insert("type".to_owned(), Value::String("string".to_owned()));
if let Some(description) = description {
property.insert(
"description".to_owned(),
Value::String(description.to_owned()),
);
}
declared.insert(name.to_owned(), Value::Object(property));
}
let mut schema = JsonObject::new();
schema.insert("type".to_owned(), Value::String("object".to_owned()));
schema.insert("properties".to_owned(), Value::Object(declared));
schema.insert("additionalProperties".to_owned(), Value::Bool(false));
if !required.is_empty() {
schema.insert(
"required".to_owned(),
Value::Array(
required
.iter()
.map(|name| Value::String((*name).to_owned()))
.collect(),
),
);
}
Arc::new(schema)
}