Skip to main content

github_mcp/prompts/
mod.rs

1//! MCP prompts exposing guided, multi-step GitHub management workflows on
2//! top of the `search`/`get`/`call` tools (see `router.rs`). Kept as its own
3//! module, separate from `tools/`, per docs/mcp-prompts-workflow-plan.md.
4
5pub mod router;
6
7use rmcp::schemars;
8use serde::Deserialize;
9
10#[derive(Debug, Deserialize, schemars::JsonSchema)]
11pub struct MasterWorkflowArgs {
12    /// What the user is trying to accomplish, in their own words (optional — omit to show the full menu)
13    pub goal: Option<String>,
14}
15
16#[derive(Debug, Deserialize, schemars::JsonSchema)]
17pub struct PullRequestWorkflowArgs {
18    /// Owner (user or organization) of the repository the pull request targets
19    pub owner: Option<String>,
20    /// Name of the repository the pull request targets
21    pub repo: Option<String>,
22    /// Branch the change should land on (e.g. "main")
23    pub base_branch: Option<String>,
24    /// Existing branch (or fork branch) carrying the change, if one already exists
25    pub head_branch: Option<String>,
26}
27
28/// Renders a short "Context already provided" header listing which of a
29/// prompt's optional arguments the caller already supplied vs. still need to
30/// be asked for. Prepended to each `content/*.md` body so the static
31/// markdown never needs its own placeholder-substitution logic.
32pub(crate) fn render_context_header(fields: &[(&str, Option<&str>)]) -> String {
33    if fields.is_empty() {
34        return String::new();
35    }
36    let mut out = String::from("## Context already provided\n");
37    let mut any_known = false;
38    for (name, value) in fields {
39        if let Some(v) = value {
40            out.push_str(&format!("- `{name}` = \"{v}\"\n"));
41            any_known = true;
42        }
43    }
44    if !any_known {
45        out.push_str("- (none — no arguments were supplied with this prompt request)\n");
46    }
47    let missing: Vec<_> = fields
48        .iter()
49        .filter(|(_, v)| v.is_none())
50        .map(|(n, _)| *n)
51        .collect();
52    if !missing.is_empty() {
53        out.push_str(&format!(
54            "\nStill unknown, ask the user before the step that needs it: {}\n",
55            missing.join(", ")
56        ));
57    }
58    out
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn empty_field_list_renders_nothing() {
67        assert_eq!(render_context_header(&[]), "");
68    }
69
70    #[test]
71    fn all_fields_supplied_lists_each_and_no_missing_section() {
72        let header =
73            render_context_header(&[("owner", Some("octocat")), ("repo", Some("hello-world"))]);
74        assert!(header.contains("`owner` = \"octocat\""));
75        assert!(header.contains("`repo` = \"hello-world\""));
76        assert!(!header.contains("Still unknown"));
77    }
78
79    #[test]
80    fn all_fields_missing_notes_none_supplied_and_lists_all_as_missing() {
81        let header = render_context_header(&[("owner", None), ("repo", None)]);
82        assert!(header.contains("(none — no arguments were supplied"));
83        assert!(
84            header
85                .contains("Still unknown, ask the user before the step that needs it: owner, repo")
86        );
87    }
88
89    #[test]
90    fn mixed_fields_report_supplied_and_missing_separately() {
91        let header = render_context_header(&[("owner", Some("octocat")), ("repo", None)]);
92        assert!(header.contains("`owner` = \"octocat\""));
93        assert!(!header.contains("`repo` ="));
94        assert!(header.contains("Still unknown, ask the user before the step that needs it: repo"));
95    }
96}