Skip to main content

xtask/rmcp_release_monitor/
options.rs

1//! CLI surface for `cargo xtask rmcp-release-monitor`.
2//!
3//! Parses the flag list into `Options`, then reads the referenced files into
4//! the two watch inputs. Keeping the flag table here means adding a monitor
5//! input touches one module instead of the orchestrator.
6
7use anyhow::{Context, Result, bail};
8use std::fs;
9use std::path::PathBuf;
10
11use super::DEFAULT_MAX_BODY_BYTES;
12use super::conformance::ConformanceMonitorInput;
13use super::schema::SchemaMonitorInput;
14
15#[derive(Debug)]
16pub(super) struct Options {
17    pub(super) crate_json: PathBuf,
18    pub(super) releases_json: PathBuf,
19    pub(super) issue_body: PathBuf,
20    schema_baseline: Option<PathBuf>,
21    schema_upstream: Option<PathBuf>,
22    schema_commits_json: Option<PathBuf>,
23    schema_url: String,
24    conformance_baseline: Option<PathBuf>,
25    conformance_head_json: Option<PathBuf>,
26    conformance_compare_json: Option<PathBuf>,
27    conformance_url: String,
28    pub(super) current_version: Option<String>,
29    pub(super) max_body_bytes: usize,
30}
31
32impl Options {
33    pub(super) fn parse(args: &[String]) -> Result<Self> {
34        let mut crate_json = None;
35        let mut releases_json = None;
36        let mut issue_body = None;
37        let mut schema_baseline = None;
38        let mut schema_upstream = None;
39        let mut schema_commits_json = None;
40        let mut schema_url =
41            "https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.ts"
42                .to_owned();
43        let mut conformance_baseline = None;
44        let mut conformance_head_json = None;
45        let mut conformance_compare_json = None;
46        let mut conformance_url = "https://github.com/modelcontextprotocol/conformance".to_owned();
47        let mut current_version = None;
48        let mut max_body_bytes = DEFAULT_MAX_BODY_BYTES;
49        let mut index = 0usize;
50        while index < args.len() {
51            match args[index].as_str() {
52                "--crate-json" => {
53                    index += 1;
54                    crate_json = Some(PathBuf::from(value_arg(args, index, "--crate-json")?));
55                }
56                "--releases-json" => {
57                    index += 1;
58                    releases_json = Some(PathBuf::from(value_arg(args, index, "--releases-json")?));
59                }
60                "--issue-body" => {
61                    index += 1;
62                    issue_body = Some(PathBuf::from(value_arg(args, index, "--issue-body")?));
63                }
64                "--schema-baseline" => {
65                    index += 1;
66                    schema_baseline =
67                        Some(PathBuf::from(value_arg(args, index, "--schema-baseline")?));
68                }
69                "--schema-upstream" => {
70                    index += 1;
71                    schema_upstream =
72                        Some(PathBuf::from(value_arg(args, index, "--schema-upstream")?));
73                }
74                "--schema-commits-json" => {
75                    index += 1;
76                    schema_commits_json = Some(PathBuf::from(value_arg(
77                        args,
78                        index,
79                        "--schema-commits-json",
80                    )?));
81                }
82                "--schema-url" => {
83                    index += 1;
84                    schema_url = value_arg(args, index, "--schema-url")?.to_owned();
85                }
86                "--conformance-baseline" => {
87                    index += 1;
88                    conformance_baseline = Some(PathBuf::from(value_arg(
89                        args,
90                        index,
91                        "--conformance-baseline",
92                    )?));
93                }
94                "--conformance-head-json" => {
95                    index += 1;
96                    conformance_head_json = Some(PathBuf::from(value_arg(
97                        args,
98                        index,
99                        "--conformance-head-json",
100                    )?));
101                }
102                "--conformance-compare-json" => {
103                    index += 1;
104                    conformance_compare_json = Some(PathBuf::from(value_arg(
105                        args,
106                        index,
107                        "--conformance-compare-json",
108                    )?));
109                }
110                "--conformance-url" => {
111                    index += 1;
112                    conformance_url = value_arg(args, index, "--conformance-url")?.to_owned();
113                }
114                "--current-version" => {
115                    index += 1;
116                    current_version = Some(value_arg(args, index, "--current-version")?.to_owned());
117                }
118                "--max-body-bytes" => {
119                    index += 1;
120                    max_body_bytes = value_arg(args, index, "--max-body-bytes")?
121                        .parse::<usize>()
122                        .context("--max-body-bytes must be an integer")?;
123                }
124                "--help" | "-h" => bail!(
125                    "Usage: cargo xtask rmcp-release-monitor --crate-json rmcp.json --releases-json releases.json --issue-body issue.md [--schema-baseline schema.ts --schema-upstream upstream.ts] [--conformance-baseline main.sha --conformance-head-json head.json] [--current-version VERSION] [--max-body-bytes N]"
126                ),
127                unknown => bail!("unknown rmcp-release-monitor option: {unknown}"),
128            }
129            index += 1;
130        }
131        Ok(Self {
132            crate_json: crate_json.context("--crate-json is required")?,
133            releases_json: releases_json.context("--releases-json is required")?,
134            issue_body: issue_body.context("--issue-body is required")?,
135            schema_baseline,
136            schema_upstream,
137            schema_commits_json,
138            schema_url,
139            conformance_baseline,
140            conformance_head_json,
141            conformance_compare_json,
142            conformance_url,
143            current_version,
144            max_body_bytes,
145        })
146    }
147
148    pub(super) fn schema_input(&self) -> Result<Option<SchemaMonitorInput>> {
149        match (&self.schema_baseline, &self.schema_upstream) {
150            (Some(baseline), Some(upstream)) => Ok(Some(SchemaMonitorInput {
151                baseline: fs::read_to_string(baseline)
152                    .with_context(|| format!("failed to read {}", baseline.display()))?,
153                upstream: fs::read_to_string(upstream)
154                    .with_context(|| format!("failed to read {}", upstream.display()))?,
155                commits_json: self
156                    .schema_commits_json
157                    .as_ref()
158                    .map(|path| {
159                        fs::read_to_string(path)
160                            .with_context(|| format!("failed to read {}", path.display()))
161                    })
162                    .transpose()?,
163                url: self.schema_url.clone(),
164                repo_root: PathBuf::from("."),
165            })),
166            (None, None) => Ok(None),
167            _ => bail!("--schema-baseline and --schema-upstream must be provided together"),
168        }
169    }
170
171    pub(super) fn conformance_input(&self) -> Result<Option<ConformanceMonitorInput>> {
172        match (&self.conformance_baseline, &self.conformance_head_json) {
173            (Some(baseline), Some(head_json)) => Ok(Some(ConformanceMonitorInput {
174                baseline_sha: fs::read_to_string(baseline)
175                    .with_context(|| format!("failed to read {}", baseline.display()))?,
176                head_json: fs::read_to_string(head_json)
177                    .with_context(|| format!("failed to read {}", head_json.display()))?,
178                compare_json: self
179                    .conformance_compare_json
180                    .as_ref()
181                    .map(|path| {
182                        fs::read_to_string(path)
183                            .with_context(|| format!("failed to read {}", path.display()))
184                    })
185                    .transpose()?,
186                url: self.conformance_url.clone(),
187                repo_root: PathBuf::from("."),
188            })),
189            (None, None) => Ok(None),
190            _ => bail!(
191                "--conformance-baseline and --conformance-head-json must be provided together"
192            ),
193        }
194    }
195}
196
197fn value_arg<'a>(args: &'a [String], index: usize, flag: &str) -> Result<&'a str> {
198    args.get(index)
199        .map(String::as_str)
200        .with_context(|| format!("{flag} requires a value"))
201}