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#[derive(Debug, Deserialize, schemars::JsonSchema)]
29pub struct RulesetsWorkflowArgs {
30    /// Account or organization the ruleset belongs to
31    pub owner_or_org: Option<String>,
32    /// Repository the ruleset targets, if it's repo-scoped (omit for an org-wide ruleset)
33    pub repo: Option<String>,
34    /// Desired name for the ruleset, if the user already has one in mind
35    pub ruleset_name: Option<String>,
36    /// Branch/tag name or pattern the ruleset should target
37    pub target_ref_pattern: Option<String>,
38}
39
40#[derive(Debug, Deserialize, schemars::JsonSchema)]
41pub struct EnvironmentsDeploymentsWorkflowArgs {
42    /// Owner (user or organization) of the repository
43    pub owner: Option<String>,
44    /// Name of the repository
45    pub repo: Option<String>,
46    /// Name of the deployment environment (e.g. "production")
47    pub environment_name: Option<String>,
48}
49
50/// Renders a short "Context already provided" header listing which of a
51/// prompt's optional arguments the caller already supplied vs. still need to
52/// be asked for. Prepended to each `content/*.md` body so the static
53/// markdown never needs its own placeholder-substitution logic.
54pub(crate) fn render_context_header(fields: &[(&str, Option<&str>)]) -> String {
55    if fields.is_empty() {
56        return String::new();
57    }
58    let mut out = String::from("## Context already provided\n");
59    let mut any_known = false;
60    for (name, value) in fields {
61        if let Some(v) = value {
62            out.push_str(&format!("- `{name}` = \"{v}\"\n"));
63            any_known = true;
64        }
65    }
66    if !any_known {
67        out.push_str("- (none — no arguments were supplied with this prompt request)\n");
68    }
69    let missing: Vec<_> = fields
70        .iter()
71        .filter(|(_, v)| v.is_none())
72        .map(|(n, _)| *n)
73        .collect();
74    if !missing.is_empty() {
75        out.push_str(&format!(
76            "\nStill unknown, ask the user before the step that needs it: {}\n",
77            missing.join(", ")
78        ));
79    }
80    out
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn empty_field_list_renders_nothing() {
89        assert_eq!(render_context_header(&[]), "");
90    }
91
92    #[test]
93    fn all_fields_supplied_lists_each_and_no_missing_section() {
94        let header =
95            render_context_header(&[("owner", Some("octocat")), ("repo", Some("hello-world"))]);
96        assert!(header.contains("`owner` = \"octocat\""));
97        assert!(header.contains("`repo` = \"hello-world\""));
98        assert!(!header.contains("Still unknown"));
99    }
100
101    #[test]
102    fn all_fields_missing_notes_none_supplied_and_lists_all_as_missing() {
103        let header = render_context_header(&[("owner", None), ("repo", None)]);
104        assert!(header.contains("(none — no arguments were supplied"));
105        assert!(
106            header
107                .contains("Still unknown, ask the user before the step that needs it: owner, repo")
108        );
109    }
110
111    #[test]
112    fn mixed_fields_report_supplied_and_missing_separately() {
113        let header = render_context_header(&[("owner", Some("octocat")), ("repo", None)]);
114        assert!(header.contains("`owner` = \"octocat\""));
115        assert!(!header.contains("`repo` ="));
116        assert!(header.contains("Still unknown, ask the user before the step that needs it: repo"));
117    }
118}