From 236742fed8f088d9cd07454b56edfb90225be205 Mon Sep 17 00:00:00 2001 From: canardleteer Date: Sun, 1 Mar 2026 18:36:16 -0800 Subject: [PATCH 1/2] feat: use clap-mcp for exposing an MCP server. --- Cargo.lock | 1137 ++++++++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 3 + src/main.rs | 115 ++--- src/misc.rs | 54 ++- src/results.rs | 44 +- 5 files changed, 1194 insertions(+), 159 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 44cea1e..f750065 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -61,11 +70,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "assert_cmd" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" dependencies = [ "anstyle", "bstr", @@ -76,12 +91,29 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bit-set" version = "0.8.0" @@ -99,9 +131,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bstr" @@ -114,27 +146,92 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" -version = "4.5.51" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", ] +[[package]] +name = "clap-mcp" +version = "0.0.3-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dd3aec6ff0e3252eeaefc528ebc60fedd9b69247aeeced27a82522fbdc7fc6" +dependencies = [ + "async-trait", + "chrono", + "clap", + "clap-mcp-macros", + "libc", + "rust-mcp-sdk", + "schemars", + "serde", + "serde_json", + "thiserror", + "tokio", +] + +[[package]] +name = "clap-mcp-macros" +version = "0.0.3-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89fed02b8d14c5a7fba0dba615b5b44af27618b08f3c3a116baf2a1805792670" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -144,9 +241,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -156,9 +253,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "colorchoice" @@ -178,12 +275,33 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -212,12 +330,112 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -226,15 +444,37 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", ] [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -242,28 +482,59 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "indexmap" -version = "2.12.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", "serde", "serde_core", ] [[package]] name = "insta" -version = "1.43.2" +version = "1.46.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" +checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4" dependencies = [ "console", "once_cell", "serde", "similar", + "tempfile", ] [[package]] @@ -285,27 +556,75 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-traits" @@ -316,6 +635,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -328,6 +656,41 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -339,9 +702,9 @@ dependencies = [ [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", @@ -350,34 +713,44 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", @@ -422,9 +795,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -435,6 +808,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.9.2" @@ -457,11 +836,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] [[package]] @@ -483,11 +862,20 @@ dependencies = [ "rand_core", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -497,9 +885,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -508,15 +896,64 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rust-mcp-schema" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ed324cdee4b735926a1eaa7741b63a5d103ee08bbe0d3701398279b0c3bf3ce" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "rust-mcp-sdk" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67357f0d9ee9c28bdb0d4cf5ae23c0359342a14d177c23b4ee992df93e9a87d7" +dependencies = [ + "async-trait", + "base64", + "bytes", + "futures", + "rust-mcp-schema", + "rust-mcp-transport", + "serde", + "serde_json", + "thiserror", + "time", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "rust-mcp-transport" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "cc5c1534d052a9ccb136539e4014645d6b3d77d8ab92ef5730cb7448c893d910" +dependencies = [ + "async-trait", + "bytes", + "futures", + "rust-mcp-schema", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tracing", +] [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -525,6 +962,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "rusty-fork" version = "0.3.1" @@ -539,25 +982,57 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] -name = "sem-tool" -version = "0.1.10" +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ - "assert_cmd", - "clap", - "clap_derive", - "indexmap", - "insta", - "insta-cmd", + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sem-tool" +version = "0.1.10" +dependencies = [ + "assert_cmd", + "clap", + "clap-mcp", + "clap_derive", + "indexmap", + "insta", + "insta-cmd", "proptest", "proptest-semver", "rand", "rand_regex", "regex", + "schemars", "semver", "serde", "serde_json", @@ -605,17 +1080,28 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -631,12 +1117,50 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "strsim" version = "0.11.1" @@ -645,9 +1169,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -656,12 +1180,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -675,24 +1199,127 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + [[package]] name = "unarray" version = "0.1.4" @@ -701,9 +1328,15 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unsafe-libyaml" @@ -717,6 +1350,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wait-timeout" version = "0.2.1" @@ -726,28 +1370,184 @@ dependencies = [ "libc", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -765,14 +1565,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -781,70 +1598,206 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index e9b8c5e..8d64c16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,9 @@ serde_json = "1.0.138" # taken the time to figure out how to make it stop. serde_yaml = "0.9.34" +clap-mcp = { version = "0.0.3-rc.1", features = ["output-schema"] } +schemars = { version = "0.8", features = ["derive"] } + [dev-dependencies] assert_cmd = "2.0.16" insta = { version = "1.42.1", features = ["json", "yaml"] } diff --git a/src/main.rs b/src/main.rs index 7d42008..19ee7eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ #![allow(rustdoc::bare_urls)] use clap::{Parser, Subcommand}; +use clap_mcp::ClapMcp; use semver::{Version, VersionReq}; use std::error::Error; use std::io; @@ -32,18 +33,23 @@ mod results; use misc::*; use results::*; -#[derive(Parser, Debug)] +#[derive(Parser, Debug, ClapMcp)] +#[clap_mcp(skip_root_when_subcommands)] #[command(version, about, long_about = None)] pub struct Args { #[command(subcommand)] cmd: Commands, + #[clap_mcp(skip)] #[clap(long, short = 'o', value_enum, default_value_t=OutputFormat::Yaml)] out: OutputFormat, } /// All commands available -#[derive(Subcommand, Debug, Clone)] +#[derive(Subcommand, Debug, Clone, ClapMcp)] +#[clap_mcp(reinvocation_safe, parallel_safe)] +#[clap_mcp_output_from = "run"] +#[clap_mcp_output_type = "McpToolOutput"] pub enum Commands { /// Explain a valid Semantic Version as parsed by the spec. /// @@ -100,6 +106,7 @@ pub enum Commands { /// /// Results are grouped by default, under the meaningful components of Semantic /// Versioning (without build metadata), then enumerated under that component. + #[clap_mcp(requires = "versions")] Sort { #[clap(long, short = 'f', default_value = None)] /// Only emit versions that match a filter. @@ -247,31 +254,41 @@ pub enum Commands { } fn main() -> Result> { - let args = Args::parse(); + let args = clap_mcp::parse_or_serve_mcp_attr::(); + + let cmd_clone = args.cmd.clone(); + let result = run(args.cmd)?; + + let ignore_exit_status_from_output = match (&cmd_clone, &result) { + ( + Commands::Compare { + set_exit_status, + semantic_exit_status, + .. + }, + SubcommandResult::ComparisonStatement(c), + ) => { + !set_exit_status + || (*semantic_exit_status && c.semantic_ordering() == &SerializableOrdering::Equal) + } + _ => false, + }; + + print!("{}", args.out.format_result(&result)?); - let mut ignore_exit_status_from_output = false; + Ok(ExitOutcome::new(result, ignore_exit_status_from_output)) +} - let result: SubcommandResult = match args.cmd { +/// Run a subcommand and produce a result. Used by both CLI and MCP. +fn run(cmd: Commands) -> Result { + let result: SubcommandResult = match cmd { Commands::Explain { semantic_version } => explain(&semantic_version).into(), Commands::Compare { - set_exit_status, - semantic_exit_status, + set_exit_status: _, + semantic_exit_status: _, a, b, - } => { - // If we don't consider non-equivalence an error, don't report one - // on process exit. - if !set_exit_status { - ignore_exit_status_from_output = true; - } - let res = compare(&a, &b); - - if semantic_exit_status && res.semantic_ordering() == &SerializableOrdering::Equal { - ignore_exit_status_from_output = true - } - - res.into() - } + } => compare(&a, &b).into(), Commands::Sort { versions, filter, @@ -282,7 +299,6 @@ fn main() -> Result> { } => { let mut parsed_versions = Vec::new(); - // Read from stdin, or pass forward the pre-parsed list from the arguments match versions { Some(versions) => parsed_versions = versions, None => { @@ -291,20 +307,18 @@ fn main() -> Result> { match line { Ok(line) => { let line = line.trim(); - parsed_versions.push(Version::parse(line) - .map_err(|e| { - eprintln!("unable to parse an enumerated version: line {line_no}: {line}: {e}"); - e + parsed_versions.push(Version::parse(line).map_err(|e| { + ApplicationError::InvalidArgument { + expected: format!("valid semver at line {line_no}: {line}"), + found: e.to_string(), + } })?); Ok(()) } - Err(e) => { - eprintln!("unable to read from stdin: {e}"); - Err(ApplicationError::InvalidArgument { - expected: "to be able to read from stdin".to_string(), - found: e.to_string(), - }) - } + Err(e) => Err(ApplicationError::InvalidArgument { + expected: "to be able to read from stdin".to_string(), + found: e.to_string(), + }), }? } } @@ -314,9 +328,9 @@ fn main() -> Result> { sort(&mut parsed_versions, &filter, lexical_sorting, reverse); if fail_if_potentially_ambiguous && ordered_version_list.potentially_ambiguous() { - return Err(Box::new(misc::ApplicationError::FailedRequirementError { + return Err(ApplicationError::FailedRequirementError { err: "Potential Ambiguity Detected".to_string(), - })); + }); } if flatten { @@ -345,32 +359,25 @@ fn main() -> Result> { set_patch, set_pre_release, set_build_metadata, - )? + ) + .map_err(|e| ApplicationError::InvalidArgument { + expected: "valid set operation".to_string(), + found: e.to_string(), + })? .into(), Commands::Bump { semantic_version, bump_major, bump_minor, bump_patch, - } => bump(&semantic_version, bump_major, bump_minor, bump_patch)?.into(), + } => bump(&semantic_version, bump_major, bump_minor, bump_patch) + .map_err(|e| ApplicationError::InvalidArgument { + expected: "valid bump operation".to_string(), + found: e.to_string(), + })? + .into(), }; - - match args.out { - OutputFormat::Text => print!("{result}"), - OutputFormat::Yaml => { - println!("---"); - let yaml = serde_yaml::to_string(&result) - .map_err(|e| ApplicationError::OutputFormatError { err: e.to_string() })?; - print!("{yaml}"); - } - OutputFormat::Json => { - let json = serde_json::to_string(&result) - .map_err(|e| ApplicationError::OutputFormatError { err: e.to_string() })?; - println!("{json}"); - } - } - - Ok(ExitOutcome::new(result, ignore_exit_status_from_output)) + Ok(result) } fn sort( diff --git a/src/misc.rs b/src/misc.rs index f5d193f..4eb5864 100644 --- a/src/misc.rs +++ b/src/misc.rs @@ -15,8 +15,10 @@ //! //! This is a bunch of last mile display + serialization logic. use clap::ValueEnum; +use clap_mcp::{ClapMcpToolError, ClapMcpToolOutput, IntoClapMcpResult, IntoClapMcpToolError}; use core::fmt; use serde::Serialize; +use schemars::JsonSchema; use std::process::{ExitCode, Termination}; use thiserror::Error; @@ -44,6 +46,23 @@ pub(crate) enum OutputFormat { Json, } +impl OutputFormat { + /// Format a subcommand result for CLI stdout. Not used for MCP (serialization handled there). + pub(crate) fn format_result( + &self, + result: &SubcommandResult, + ) -> Result { + match self { + OutputFormat::Text => Ok(result.to_string()), + OutputFormat::Yaml => serde_yaml::to_string(result) + .map_err(|e| ApplicationError::OutputFormatError { err: e.to_string() }) + .map(|s| format!("---\n{s}")), + OutputFormat::Json => serde_json::to_string(result) + .map_err(|e| ApplicationError::OutputFormatError { err: e.to_string() }), + } + } +} + /// Trait for subcommand output: types that can be written to stdout and report an exit code. /// Each subcommand selects one of these types; the concrete type is preserved in [SubcommandResult]. #[allow(dead_code)] // bound for subcommand result types; used for documentation and future accessors @@ -76,7 +95,7 @@ impl Termination for ExitOutcome { } } -#[derive(Serialize)] +#[derive(Serialize, JsonSchema)] #[serde(untagged)] pub(crate) enum SubcommandResult { /// Assertion by this program @@ -144,6 +163,39 @@ impl From for SubcommandResult { } } +/// MCP tool response wrapper: explicit `ok` for exit-code-like semantics (Validate, FilterTest, Compare). +#[derive(Serialize, JsonSchema)] +pub(crate) struct McpToolOutput { + pub(crate) ok: bool, + pub(crate) result: SubcommandResult, +} + +/// Compute success for MCP: true for exit-code success, so clients get explicit semantics. +fn success_for_mcp(result: &SubcommandResult) -> bool { + match result { + SubcommandResult::ValidateResult(v) => v.success(), + SubcommandResult::FilterTestResult(f) => f.success(), + SubcommandResult::ComparisonStatement(c) => c.both_equal(), + _ => true, + } +} + +impl IntoClapMcpResult for SubcommandResult { + fn into_tool_result(self) -> std::result::Result { + let ok = success_for_mcp(&self); + let wrapped = McpToolOutput { ok, result: self }; + serde_json::to_value(&wrapped) + .map(ClapMcpToolOutput::Structured) + .map_err(|e| ClapMcpToolError::text(e.to_string())) + } +} + +impl IntoClapMcpToolError for ApplicationError { + fn into_tool_error(self) -> ClapMcpToolError { + ClapMcpToolError::text(self.to_string()) + } +} + impl Termination for SubcommandResult { // NOTE(canardleteer): only expected to be called along certain code paths // (at least for now). diff --git a/src/results.rs b/src/results.rs index 791d0ee..5a4efa7 100644 --- a/src/results.rs +++ b/src/results.rs @@ -26,11 +26,12 @@ use rand::prelude::*; use regex::Regex; use semver::{BuildMetadata, Prerelease, Version, VersionReq}; use serde::Serialize; +use schemars::JsonSchema; use super::regex::{generate_any_valid_semver, generate_u64_safe_semver}; /// The result of a simple filter test. -#[derive(Serialize, PartialEq)] +#[derive(Serialize, JsonSchema, PartialEq)] pub(crate) struct ValidateResult { valid: bool, } @@ -48,6 +49,11 @@ impl ValidateResult { Self { valid: pass } } + + /// True if validation passed (exit-code success). Used for MCP explicit success wrapper. + pub(crate) fn success(&self) -> bool { + self.valid + } } impl fmt::Display for ValidateResult { @@ -71,7 +77,7 @@ impl Termination for ValidateResult { } /// The result of a simple filter test. -#[derive(Serialize, PartialEq)] +#[derive(Serialize, JsonSchema, PartialEq)] pub(crate) struct FilterTestResult { pass: bool, } @@ -80,6 +86,11 @@ impl FilterTestResult { pub(crate) fn filter_test(filter: &VersionReq, semantic_version: &Version) -> FilterTestResult { filter.matches(semantic_version).into() } + + /// True if filter matched (exit-code success). Used for MCP explicit success wrapper. + pub(crate) fn success(&self) -> bool { + self.pass + } } /// A equivalent of an ExitCode, for true/false. @@ -108,7 +119,7 @@ impl fmt::Display for FilterTestResult { } } -#[derive(Clone, Debug, Serialize, PartialEq)] +#[derive(Clone, Debug, Serialize, JsonSchema, PartialEq)] pub(crate) enum SegmentType { Numeric, Ascii, @@ -126,7 +137,7 @@ impl fmt::Display for SegmentType { /// Describes a dot separated segment of either a Pre-Release, or Build Metadata string. /// /// Kind describes how the value is meant to be interpreted for precedence. -#[derive(Clone, Debug, Serialize, PartialEq)] +#[derive(Clone, Debug, Serialize, JsonSchema, PartialEq)] pub(crate) struct PreMetaSegment { kind: SegmentType, value: String, @@ -153,7 +164,7 @@ impl From<&str> for PreMetaSegment { } /// Descriptive information about a Version. -#[derive(Clone, Serialize, PartialEq)] +#[derive(Clone, Serialize, JsonSchema, PartialEq)] pub(crate) struct VersionExplanation { major: u64, minor: u64, @@ -243,8 +254,9 @@ impl Termination for VersionExplanation { } /// A simple list of Versions. -#[derive(Serialize, PartialEq)] +#[derive(Serialize, JsonSchema, PartialEq)] pub(crate) struct FlatVersionsList { + #[schemars(with = "Vec")] versions: Vec, potentially_ambiguous: bool, } @@ -279,7 +291,7 @@ impl Termination for FlatVersionsList { /// A simple list of Strings. /// /// NOTE(canardleteer): Probably could become any serializable type with Display. -#[derive(Serialize, PartialEq)] +#[derive(Serialize, JsonSchema, PartialEq)] pub(crate) struct FlatStringList { versions: Vec, } @@ -308,8 +320,9 @@ impl Termination for FlatStringList { } /// A usefully ordered list of versions. -#[derive(Serialize)] +#[derive(Serialize, JsonSchema)] pub(crate) struct OrderedVersionMap { + #[schemars(rename = "versions", with = "std::collections::HashMap>")] #[serde(rename(serialize = "versions"))] inner: IndexMap>, potentially_ambiguous: bool, @@ -403,7 +416,7 @@ impl Termination for OrderedVersionMap { } /// A statement about the comparison about 2 versions -#[derive(Serialize, PartialEq)] +#[derive(Serialize, JsonSchema, PartialEq)] pub(crate) struct ComparisonStatement { semantic_ordering: SerializableOrdering, lexical_ordering: SerializableOrdering, @@ -428,9 +441,15 @@ impl ComparisonStatement { pub(crate) fn lexical_ordering(&self) -> &SerializableOrdering { &self.lexical_ordering } + + /// True when both semantic and lexical orderings are Equal (exit-code success). Used for MCP explicit success wrapper. + pub(crate) fn both_equal(&self) -> bool { + *self.semantic_ordering() == SerializableOrdering::Equal + && *self.lexical_ordering() == SerializableOrdering::Equal + } } -#[derive(Serialize, PartialEq)] +#[derive(Serialize, JsonSchema, PartialEq)] pub(crate) struct GenerateResult { inner: Vec, } @@ -459,8 +478,9 @@ impl fmt::Display for GenerateResult { } } -#[derive(Serialize, PartialEq)] +#[derive(Serialize, JsonSchema, PartialEq)] pub(crate) struct VersionMutationResult { + #[schemars(with = "String")] pub(crate) mutated_version: Version, } @@ -625,7 +645,7 @@ impl fmt::Display for ComparisonStatement { } /// Just a small reimplementation of std::Ordering with Serialization. -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, JsonSchema, PartialEq)] pub(crate) enum SerializableOrdering { Less, Greater, From 98a11ba6df37163d7f2aeec3f27b15203cc7f3b0 Mon Sep 17 00:00:00 2001 From: canardleteer Date: Sun, 8 Mar 2026 16:09:44 -0700 Subject: [PATCH 2/2] feat: mcp as a feature, additional release binary --- .github/workflows/release-plz.yml | 2 + .github/workflows/testing.yml | 13 +- .gitignore | 1 + .release-plz.toml | 16 +- Cargo.lock | 8 + Cargo.toml | 13 +- README.md | 33 +- dist-workspace.toml | 2 +- sem-tool-mcp/Cargo.toml | 14 + sem-tool-mcp/src/main.rs | 14 + src/lib.rs | 488 +++++++++++++++++ src/main.rs | 495 +----------------- src/misc.rs | 15 +- src/results.rs | 58 +- tests/cli_insta.rs | 59 ++- tests/snapshots/cli_insta__help.bump.snap | 35 ++ tests/snapshots/cli_insta__help.compare.snap | 49 ++ tests/snapshots/cli_insta__help.explain.snap | 32 ++ .../cli_insta__help.filter-test.snap | 35 ++ tests/snapshots/cli_insta__help.generate.snap | 23 + tests/snapshots/cli_insta__help.root.snap | 31 ++ tests/snapshots/cli_insta__help.set.snap | 27 + tests/snapshots/cli_insta__help.sort.snap | 63 +++ tests/snapshots/cli_insta__help.validate.snap | 29 + tests/snapshots/cli_insta__help_mcp.bump.snap | 41 ++ .../cli_insta__help_mcp.compare.snap | 55 ++ .../cli_insta__help_mcp.explain.snap | 38 ++ .../cli_insta__help_mcp.filter-test.snap | 41 ++ .../cli_insta__help_mcp.generate.snap | 25 + tests/snapshots/cli_insta__help_mcp.root.snap | 33 ++ tests/snapshots/cli_insta__help_mcp.set.snap | 37 ++ tests/snapshots/cli_insta__help_mcp.sort.snap | 69 +++ .../cli_insta__help_mcp.validate.snap | 35 ++ .../snapshots/cli_insta__sort.complex.1.snap | 2 +- 34 files changed, 1404 insertions(+), 527 deletions(-) create mode 100644 sem-tool-mcp/Cargo.toml create mode 100644 sem-tool-mcp/src/main.rs create mode 100644 src/lib.rs create mode 100644 tests/snapshots/cli_insta__help.bump.snap create mode 100644 tests/snapshots/cli_insta__help.compare.snap create mode 100644 tests/snapshots/cli_insta__help.explain.snap create mode 100644 tests/snapshots/cli_insta__help.filter-test.snap create mode 100644 tests/snapshots/cli_insta__help.generate.snap create mode 100644 tests/snapshots/cli_insta__help.root.snap create mode 100644 tests/snapshots/cli_insta__help.set.snap create mode 100644 tests/snapshots/cli_insta__help.sort.snap create mode 100644 tests/snapshots/cli_insta__help.validate.snap create mode 100644 tests/snapshots/cli_insta__help_mcp.bump.snap create mode 100644 tests/snapshots/cli_insta__help_mcp.compare.snap create mode 100644 tests/snapshots/cli_insta__help_mcp.explain.snap create mode 100644 tests/snapshots/cli_insta__help_mcp.filter-test.snap create mode 100644 tests/snapshots/cli_insta__help_mcp.generate.snap create mode 100644 tests/snapshots/cli_insta__help_mcp.root.snap create mode 100644 tests/snapshots/cli_insta__help_mcp.set.snap create mode 100644 tests/snapshots/cli_insta__help_mcp.sort.snap create mode 100644 tests/snapshots/cli_insta__help_mcp.validate.snap diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 0ab7ff0..e767dd9 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -20,6 +20,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - run: cargo test + - run: cargo test --features mcp - name: Run release-plz uses: release-plz/action@v0.5 with: @@ -46,6 +47,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - run: cargo test + - run: cargo test --features mcp - name: Run release-plz uses: release-plz/action@v0.5 with: diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index ac6609d..376ed87 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -8,6 +8,7 @@ jobs: strategy: matrix: platform: [macos-latest, ubuntu-latest, windows-latest] + features: [default, mcp] runs-on: ${{ matrix.platform }} steps: @@ -20,7 +21,7 @@ jobs: with: path: | ~/target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-${{ matrix.features }} # - name: Setup pnpm # uses: pnpm/action-setup@v2 @@ -38,8 +39,12 @@ jobs: - name: Run test suite working-directory: ./ - run: cargo test - + run: cargo test ${{ matrix.features == 'mcp' && '--features mcp' || '' }} + env: + # When testing the mcp feature build, skip default insta snapshots (help/output + # diverges due to --mcp, --export-skills, etc.) and run mcp-specific help snapshots instead. + SEM_TOOL_SKIP_INSTA: ${{ matrix.features == 'mcp' && '1' || '' }} + SEM_TOOL_MCP_HELP: ${{ matrix.features == 'mcp' && '1' || '' }} - name: Do a simple build working-directory: ./ - run: cargo build \ No newline at end of file + run: cargo build ${{ matrix.features == 'mcp' && '--features mcp' || '' }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index bef4516..2cfaa99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .vscode /target +*.snap.new diff --git a/.release-plz.toml b/.release-plz.toml index efa9c08..35c6e3b 100644 --- a/.release-plz.toml +++ b/.release-plz.toml @@ -1,7 +1,13 @@ [[package]] -name="sem-tool" +name = "sem-tool" +version_group = "sem-tool" -# `git_release_enable` is used by the `release-plz` tool to enable/disable -# some form of "Release API", but we allow `dist` to publish the `release` -# on Github. -git_release_enable = false \ No newline at end of file +# release-plz still drives the release (versioning, tagging, crates.io publish). +# We leave GitHub Release creation to dist: the tag pushed by release-plz +# triggers the Release workflow, and dist builds artifacts and creates the +# GitHub Release with installers/binaries. Avoids duplicate or empty releases. +git_release_enable = false + +[[package]] +name = "sem-tool-mcp" +version_group = "sem-tool" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f750065..a490efe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -532,6 +532,7 @@ checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4" dependencies = [ "console", "once_cell", + "regex", "serde", "similar", "tempfile", @@ -1040,6 +1041,13 @@ dependencies = [ "thiserror", ] +[[package]] +name = "sem-tool-mcp" +version = "0.1.10" +dependencies = [ + "sem-tool", +] + [[package]] name = "semver" version = "1.0.27" diff --git a/Cargo.toml b/Cargo.toml index 8d64c16..d4412b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,13 @@ categories = ["command-line-utilities"] readme = "README.md" description = "A simple Semantic Versioning CLI tool for parsing, filtering, sorting and explaining" +[workspace] +members = [".", "sem-tool-mcp"] + +[features] +# Experimental: enable MCP server mode (--mcp / --export-skills). Not enabled by default. +mcp = ["dep:clap-mcp", "dep:schemars"] + [package.metadata.wix] upgrade-guid = "4B0BC255-6C49-4242-8F92-789262A65064" path-guid = "D28BFC08-1707-4BF8-ACB8-9A4496178C5F" @@ -63,12 +70,12 @@ serde_json = "1.0.138" # taken the time to figure out how to make it stop. serde_yaml = "0.9.34" -clap-mcp = { version = "0.0.3-rc.1", features = ["output-schema"] } -schemars = { version = "0.8", features = ["derive"] } +clap-mcp = { version = "0.0.3-rc.1", optional = true, features = ["output-schema"] } +schemars = { version = "0.8", optional = true, features = ["derive"] } [dev-dependencies] assert_cmd = "2.0.16" -insta = { version = "1.42.1", features = ["json", "yaml"] } +insta = { version = "1.42.1", features = ["json", "yaml", "filters"] } insta-cmd = "0.6.0" proptest = "1.6.0" proptest-semver = "0.1.2" diff --git a/README.md b/README.md index 448ec92..c71ceb5 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,15 @@ interpretation. cargo install sem-tool ``` -- [Releases](https://github.com/canardleteer/sem-tool/releases) have various installer - patterns for multiple Operating Systems and Package Management tools. +This installs the CLI only (no MCP server support). To enable **MCP** (Model Context Protocol) server mode (e.g. `--mcp` / `--export-skills`), install with the optional feature: + +```shell +cargo install sem-tool --features mcp +``` + +The `mcp` feature is **experimental** and is not enabled by default. + +- [Releases](https://github.com/canardleteer/sem-tool/releases) provide installers and binaries for multiple operating systems. The **sem-tool-mcp** binary (MCP-enabled) is also published there for each release if you prefer a pre-built MCP build instead of `cargo install sem-tool --features mcp`. ## Running @@ -59,6 +66,28 @@ Your best place to start, is: sem-tool --help ``` +## Testing + +To run the same test coverage as CI locally: + +1. **Default build** (no MCP): + + ```shell + cargo test + ``` + +2. **MCP feature build** (uses separate help snapshots; same env as CI): + + ```shell + SEM_TOOL_SKIP_INSTA=1 SEM_TOOL_MCP_HELP=1 cargo test --features mcp + ``` + +To regenerate MCP help snapshots after changing CLI help or the `mcp` feature: + +```shell +SEM_TOOL_SKIP_INSTA=1 SEM_TOOL_MCP_HELP=1 INSTA_UPDATE=1 cargo test --features mcp --test cli_insta +``` + ## Output Currently, the following output types are: `yaml`, `text`, `json`. diff --git a/dist-workspace.toml b/dist-workspace.toml index 3aa417a..bd34db4 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -1,5 +1,5 @@ [workspace] -members = ["cargo:."] +members = ["cargo:.", "cargo:sem-tool-mcp"] # Config for 'dist' [dist] diff --git a/sem-tool-mcp/Cargo.toml b/sem-tool-mcp/Cargo.toml new file mode 100644 index 0000000..f66abf7 --- /dev/null +++ b/sem-tool-mcp/Cargo.toml @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2025 canardleteer +# +# Optional MCP-enabled binary for distribution only. Not published to crates.io. + +[package] +name = "sem-tool-mcp" +version = "0.1.10" +edition = "2024" +publish = false +description = "sem-tool with MCP server support (distribution binary only)" + +[dependencies] +sem-tool = { path = "..", features = ["mcp"] } diff --git a/sem-tool-mcp/src/main.rs b/sem-tool-mcp/src/main.rs new file mode 100644 index 0000000..f5ae6d2 --- /dev/null +++ b/sem-tool-mcp/src/main.rs @@ -0,0 +1,14 @@ +//! SPDX-License-Identifier: Apache-2.0 +//! Copyright 2025 canardleteer +//! +//! Binary entrypoint for sem-tool-mcp; delegates to sem-tool with MCP feature. + +fn main() -> std::process::ExitCode { + match sem_tool::run_app() { + Ok(exit_code) => exit_code, + Err(e) => { + eprintln!("{e}"); + std::process::ExitCode::FAILURE + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..df370f6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,488 @@ +//! SPDX-License-Identifier: Apache-2.0 +//! Copyright 2025 canardleteer +//! +//! Licensed under the Apache License, Version 2.0 (the "License"); +//! you may not use this file except in compliance with the License. +//! You may obtain a copy of the License at +//! +//! http://www.apache.org/licenses/LICENSE-2.0 +//! +//! Unless required by applicable law or agreed to in writing, software +//! distributed under the License is distributed on an "AS IS" BASIS, +//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//! See the License for the specific language governing permissions and +//! limitations under the License. +//! +//! Library entrypoint and CLI types for sem-tool. + +#![allow(rustdoc::bare_urls)] + +use clap::{Parser, Subcommand}; +#[cfg(feature = "mcp")] +use clap_mcp::ClapMcp; +use semver::{Version, VersionReq}; +use std::error::Error; +use std::io; +use std::process::Termination; + +pub mod misc; +mod regex; +pub mod results; + +use misc::*; +use results::*; + +/// A simple Semantic Versioning CLI tool for parsing, filtering, sorting and explaining. +#[derive(Parser, Debug)] +#[cfg_attr(feature = "mcp", derive(ClapMcp))] +#[cfg_attr(feature = "mcp", clap_mcp(skip_root_when_subcommands))] +#[command(version, about, long_about = None)] +pub struct Args { + #[command(subcommand)] + pub cmd: Commands, + + #[cfg_attr(feature = "mcp", clap_mcp(skip))] + /// Output format (yaml, text, json). + #[clap(long, short = 'o', value_enum, default_value_t = OutputFormat::Yaml)] + pub out: OutputFormat, +} + +/// All commands available +#[derive(Subcommand, Debug, Clone)] +#[cfg_attr(feature = "mcp", derive(ClapMcp))] +#[cfg_attr(feature = "mcp", clap_mcp(reinvocation_safe, parallel_safe))] +#[cfg_attr(feature = "mcp", clap_mcp_output_from = "run")] +#[cfg_attr(feature = "mcp", clap_mcp_output_type = "McpToolOutput")] +pub enum Commands { + /// Explain a valid Semantic Version as parsed by the spec. + /// + /// Breaks apart the Semantic Version, into it's individual components. + /// + /// All values are returned as strings, because the unsigned integer + /// types are not necessarily bound by a numeric type that is parsable + /// by common libraries. + /// + /// It is worth noting, Semver 2.0.0 §11.4.1 & §11.4.2 pre-release & + /// metadata dot separated values, cannot be negative numbers, since + /// they cannot be represented with hyphens. + /// + /// Reference: https://semver.org/#spec-item-11 + /// + Explain { semantic_version: Version }, + /// Compare 2 Semantic Versions. + /// + /// Results are provided in the form + /// "A is {Greater,Equals,Less} {to,than} B", with both Semantic results + /// (meaningful results under Semantic Versioning), as well as Lexical + /// results (meaningless, but handy for sorting text lists). + /// + /// Without enabling `--set_exit_status`, the exit status is generally + /// meaningless, other than confirming that the arguments were valid. + Compare { + /// If you want some slightly complex exit status codes for this dual + /// compare, you can turn them on with this flag. + /// + /// When both Semantic and Lexical comparisons are Equal, the command + /// will end with an exit status of 0 (Success). + /// + /// All other outcomes, are returned with an exit status of the form: 1XY [between 100-122]. + /// + /// - With X being (0 if Less, 1 if Equal, 2 if Greater) on the Semantic Compare + /// + /// - With Y being (0 if Less, 1 if Equal, 2 if Greater) on the Lexical Compare + /// + /// The non-0 exit status codes, should be considered UNSTABLE, because something + /// better can probably be figured out. + #[clap(long, short = 'e', action)] + set_exit_status: bool, + /// Always exit with success when Semantic Versions are Equal. + /// + /// Mostly impacts the output when the flag `set_exit_status` is set. + #[clap(long, short = 's', action)] + semantic_exit_status: bool, + /// The base version used for comparison. + a: Version, + /// The version we are comparing against. + b: Version, + }, + /// Sort a list of valid Semantic Versions, with either Semantic or Lexical ordering. + /// + /// Results are grouped by default, under the meaningful components of Semantic + /// Versioning (without build metadata), then enumerated under that component. + #[cfg_attr(feature = "mcp", clap_mcp(requires = "versions"))] + Sort { + #[clap(long, short = 'f', default_value = None)] + /// Only emit versions that match a filter. + /// + /// These filter rules are described by the semver crate `VersionReq` + /// documentation, and more generally in the cargo book. + /// + /// In particular, note the warnings around pre-releases in the + /// VersionReq documentation. + /// + /// References: + /// - https://docs.rs/semver/1.0.25/semver/struct.VersionReq.html + /// - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html + filter: Option, + + #[clap(long, action)] + /// Lexical Sorting (aka Total Order). + /// + /// WARNING: This may lead to bad choices surrounding semantic + /// versioning. + /// + /// Semver 2.0.0 §10 states that: + /// "Build metadata MUST be ignored when determining version + /// precedence." + /// + /// This has been set to the default behavior of emulating undefined + /// behavior, because it MUST be ignored. It is quite common, for + /// people to accidentally choose the sorting order of their favorite + /// or most familiar tool, and not the specification itself. + /// + /// Additionally, we must interpret the following statement as + /// undefined ordering for the case where Build Metadata may be `None` + /// or `Some`: + /// + /// "Thus two versions that differ only in the build metadata, have + /// the same precedence." + /// + /// References: + /// - https://semver.org/#spec-item-10 + lexical_sorting: bool, + + #[clap(long, short = 'r', action)] + /// Reverses ordering. + /// + /// Note, "reversing" always effects the comparable versions being + /// ordered, but is ignored when NOT lexically sorted, for the list of + /// semantically identical versions (aka, different metadata). Since by + /// default they are randomly sorted, there is no point. + reverse: bool, + + #[clap(long, action)] + /// Flatten the map, and provide a list of versions. + /// + /// WARNING: This may lead to bad choices surrounding semantic + /// versioning. + flatten: bool, + + #[clap(long, action)] + /// Fail, if potentially ambiguous precedence may emerge from these + /// versions (multiple matching M.M.P-PR, but non-matching metadata). + fail_if_potentially_ambiguous: bool, + + /// If no versions are present, then the tool will read from stdin, one + /// version per line. + versions: Option>, + }, + /// Test a Semantic Version against a filter + FilterTest { + /// Filter to test against a specific Semantic Version. + /// + /// These filter rules are described by the semver crate `VersionReq` + /// documentation, and more generally in the cargo book. + /// + /// In particular, note the warnings around pre-releases in the + /// VersionReq documentation. + /// + /// The Status Code will be 0 if it passes, non-zero if it fails. + /// + /// References: + /// - https://docs.rs/semver/1.0.25/semver/struct.VersionReq.html + /// - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html + filter: VersionReq, + + /// Version to test + semantic_version: Version, + }, + /// Simply validates an argument, to confirm it is a valid Semantic Version + /// + /// The Status Code will be 0 if it is valid, non-zero if it is not. + Validate { + /// Version to validate + version: String, + + /// "Small" will ensure the MAJOR, MINOR & PATCH components are under [u64::MAX]. + #[clap(long, short = 's', action)] + small: bool, + }, + /// Generate random & valid Semantic Version Strings. They may be large and awkward, but valid Semantic Versions. Useful for testing and validation. + Generate { + /// "Small" will ensure the MAJOR, MINOR & PATCH components are under [u64::MAX]. + #[clap(long, short = 's', action)] + small: bool, + + /// How many to create (default 1) + #[clap(default_value_t = 1)] + count: usize, + }, + /// Set commands will replace one segment of semver, with a specified one, + /// and print out the mutated version. + Set { + semantic_version: Version, + + #[clap(long, action)] + set_major: Option, + #[clap(long, action)] + set_minor: Option, + #[clap(long, action)] + set_patch: Option, + #[clap(long, action)] + set_pre_release: Option, + #[clap(long, action)] + set_build_metadata: Option, + }, + /// Bump commands will increment one segment of semver by the specified + /// amount, and print out the mutated version. + /// + /// Only Major, Minor and Patch are supported. You'll want to consider the + /// `set` subcommand for pre-release and build-metadata. + Bump { + semantic_version: Version, + + #[clap(long, action)] + bump_major: Option, + #[clap(long, action)] + bump_minor: Option, + #[clap(long, action)] + bump_patch: Option, + }, +} + +/// Library entrypoint: parse args (CLI or MCP per feature), run command, format and return exit code. +pub fn run_app() -> Result> { + #[cfg(feature = "mcp")] + let args = clap_mcp::parse_or_serve_mcp_attr::(); + #[cfg(not(feature = "mcp"))] + let args = Args::parse(); + + let cmd_clone = args.cmd.clone(); + let result = run(args.cmd)?; + + let ignore_exit_status_from_output = match (&cmd_clone, &result) { + ( + Commands::Compare { + set_exit_status, + semantic_exit_status, + .. + }, + SubcommandResult::ComparisonStatement(c), + ) => { + !set_exit_status + || (*semantic_exit_status && c.semantic_ordering() == &SerializableOrdering::Equal) + } + _ => false, + }; + + print!("{}", args.out.format_result(&result)?); + + let outcome = ExitOutcome::new(result, ignore_exit_status_from_output); + Ok(outcome.report()) +} + +/// Run a subcommand and produce a result. Used by both CLI and MCP. +pub(crate) fn run(cmd: Commands) -> Result { + let result: SubcommandResult = match cmd { + Commands::Explain { semantic_version } => explain(&semantic_version).into(), + Commands::Compare { + set_exit_status: _, + semantic_exit_status: _, + a, + b, + } => compare(&a, &b).into(), + Commands::Sort { + versions, + filter, + lexical_sorting, + reverse, + flatten, + fail_if_potentially_ambiguous, + } => { + let mut parsed_versions = Vec::new(); + match versions { + Some(versions) => parsed_versions = versions, + None => { + let lines = io::stdin().lines(); + for (line_no, line) in lines.enumerate() { + match line { + Ok(line) => { + let line = line.trim(); + parsed_versions.push(Version::parse(line).map_err(|e| { + ApplicationError::InvalidArgument { + expected: format!("valid semver at line {line_no}: {line}"), + found: e.to_string(), + } + })?); + Ok(()) + } + Err(e) => Err(ApplicationError::InvalidArgument { + expected: "to be able to read from stdin".to_string(), + found: e.to_string(), + }), + }? + } + } + } + let mut ordered_version_list = + sort(&mut parsed_versions, &filter, lexical_sorting, reverse); + if fail_if_potentially_ambiguous && ordered_version_list.potentially_ambiguous() { + return Err(ApplicationError::FailedRequirementError { + err: "Potential Ambiguity Detected".to_string(), + }); + } + if flatten { + FlatVersionsList::from(&mut ordered_version_list).into() + } else { + ordered_version_list.into() + } + } + Commands::FilterTest { + filter, + semantic_version, + } => filter_test(&filter, &semantic_version).into(), + Commands::Validate { version, small } => validate(version, small).into(), + Commands::Generate { small, count } => generate(small, count).into(), + Commands::Set { + semantic_version, + set_major, + set_minor, + set_patch, + set_pre_release, + set_build_metadata, + } => set( + &semantic_version, + set_major, + set_minor, + set_patch, + set_pre_release, + set_build_metadata, + ) + .map_err(|e| ApplicationError::InvalidArgument { + expected: "valid set operation".to_string(), + found: e.to_string(), + })? + .into(), + Commands::Bump { + semantic_version, + bump_major, + bump_minor, + bump_patch, + } => bump(&semantic_version, bump_major, bump_minor, bump_patch) + .map_err(|e| ApplicationError::InvalidArgument { + expected: "valid bump operation".to_string(), + found: e.to_string(), + })? + .into(), + }; + Ok(result) +} + +fn sort( + versions: &mut Vec, + filter: &Option, + lexical_sorting: bool, + reverse: bool, +) -> OrderedVersionMap { + OrderedVersionMap::new(versions, filter, lexical_sorting, reverse) +} + +fn compare(a: &Version, b: &Version) -> ComparisonStatement { + ComparisonStatement::new(a, b) +} + +fn explain(v: &Version) -> VersionExplanation { + VersionExplanation::from(v) +} + +fn filter_test(filter: &VersionReq, semantic_version: &Version) -> FilterTestResult { + FilterTestResult::filter_test(filter, semantic_version) +} + +fn validate(semantic_version: String, small: bool) -> ValidateResult { + ValidateResult::validate(semantic_version, small) +} + +fn generate(small: bool, count: usize) -> GenerateResult { + GenerateResult::new(small, count) +} + +fn set( + version: &Version, + major: Option, + minor: Option, + patch: Option, + pre_release: Option, + build_metadata: Option, +) -> Result> { + VersionMutationResult::set(version, major, minor, patch, pre_release, build_metadata) +} + +fn bump( + version: &Version, + major: Option, + minor: Option, + patch: Option, +) -> Result> { + VersionMutationResult::bump(version, major, minor, patch) +} + +pub use results::SerializableOrdering; +pub use results::version_without_build_metadata; + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + use proptest_semver::*; + + use crate::{SerializableOrdering, version_without_build_metadata}; + + proptest! { + #[test] + fn compare(a in arb_version(), b in arb_version()) { + let comparison = super::compare(&a, &b); + let a_no_build = version_without_build_metadata(&a); + let b_no_build = version_without_build_metadata(&b); + match a.cmp(&b) { + std::cmp::Ordering::Equal => { + prop_assert_eq!(a_no_build, b_no_build); + prop_assert!(comparison.semantic_ordering() == &SerializableOrdering::Equal); + }, + std::cmp::Ordering::Greater => { + prop_assert!(comparison.lexical_ordering() == &SerializableOrdering::Greater || comparison.lexical_ordering() == &SerializableOrdering::Equal); + prop_assert_eq!(comparison.semantic_ordering(), &SerializableOrdering::Greater); + }, + std::cmp::Ordering::Less => { + prop_assert!(comparison.lexical_ordering() == &SerializableOrdering::Less || comparison.lexical_ordering() == &SerializableOrdering::Equal); + prop_assert_eq!(comparison.semantic_ordering(), &SerializableOrdering::Less); + }, + }; + } + + #[test] + fn explain(version in arb_version()) { + super::explain(&version); + } + + #[test] + fn validate(version in arb_semver(), small: bool) { + super::validate(version, small); + } + + #[test] + fn filter_test(filter in arb_version_req(MAX_COMPARATORS_IN_VERSION_REQ_STRING), version in arb_version()) { + super::filter_test(&filter, &version); + } + + #[test] + fn sort(versions in arb_vec_versions(256), filter in arb_optional_version_req(0.5, MAX_COMPARATORS_IN_VERSION_REQ_STRING), lexical_sorting in any::(), reverse in any::()) { + let mut versions = versions.clone(); + super::sort(&mut versions, &filter, lexical_sorting, reverse); + } + + #[test] + fn generate(small: bool, count: u8) { + super::generate(small, count.into()); + } + } +} diff --git a/src/main.rs b/src/main.rs index 19ee7eb..714e9d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,497 +1,16 @@ //! SPDX-License-Identifier: Apache-2.0 //! Copyright 2025 canardleteer //! -//! Licensed under the Apache License, Version 2.0 (the "License"); -//! you may not use this file except in compliance with the License. -//! You may obtain a copy of the License at -//! -//! http://www.apache.org/licenses/LICENSE-2.0 -//! -//! Unless required by applicable law or agreed to in writing, software -//! distributed under the License is distributed on an "AS IS" BASIS, -//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -//! See the License for the specific language governing permissions and -//! limitations under the License. -//! -//! This source file doesn't contain much more than just the basics of -//! CLI documentation, and routing to the appropriate place. +//! Binary entrypoint for sem-tool; delegates to the library. -//! NOTE(canardleteer): We allow bare_urls, because CLI documentation is -//! more important than rust-doc here. #![allow(rustdoc::bare_urls)] -use clap::{Parser, Subcommand}; -use clap_mcp::ClapMcp; -use semver::{Version, VersionReq}; -use std::error::Error; -use std::io; - -mod misc; -mod regex; -mod results; - -use misc::*; -use results::*; - -#[derive(Parser, Debug, ClapMcp)] -#[clap_mcp(skip_root_when_subcommands)] -#[command(version, about, long_about = None)] -pub struct Args { - #[command(subcommand)] - cmd: Commands, - - #[clap_mcp(skip)] - #[clap(long, short = 'o', value_enum, default_value_t=OutputFormat::Yaml)] - out: OutputFormat, -} - -/// All commands available -#[derive(Subcommand, Debug, Clone, ClapMcp)] -#[clap_mcp(reinvocation_safe, parallel_safe)] -#[clap_mcp_output_from = "run"] -#[clap_mcp_output_type = "McpToolOutput"] -pub enum Commands { - /// Explain a valid Semantic Version as parsed by the spec. - /// - /// Breaks apart the Semantic Version, into it's individual components. - /// - /// All values are returned as strings, because the unsigned integer - /// types are not necessarily bound by a numeric type that is parsable - /// by common libraries. - /// - /// It is worth noting, Semver 2.0.0 §11.4.1 & §11.4.2 pre-release & - /// metadata dot separated values, cannot be negative numbers, since - /// they cannot be represented with hyphens. - /// - /// Reference: https://semver.org/#spec-item-11 - /// - Explain { semantic_version: Version }, - /// Compare 2 Semantic Versions. - /// - /// Results are provided in the form - /// "A is {Greater,Equals,Less} {to,than} B", with both Semantic results - /// (meaningful results under Semantic Versioning), as well as Lexical - /// results (meaningless, but handy for sorting text lists). - /// - /// Without enabling `--set_exit_status`, the exit status is generally - /// meaningless, other than confirming that the arguments were valid. - Compare { - /// If you want some slightly complex exit status codes for this dual - /// compare, you can turn them on with this flag. - /// - /// When both Semantic and Lexical comparisons are Equal, the command - /// will end with an exit status of 0 (Success). - /// - /// All other outcomes, are returned with an exit status of the form: 1XY [between 100-122]. - /// - /// - With X being (0 if Less, 1 if Equal, 2 if Greater) on the Semantic Compare - /// - /// - With Y being (0 if Less, 1 if Equal, 2 if Greater) on the Lexical Compare - /// - /// The non-0 exit status codes, should be considered UNSTABLE, because something - /// better can probably be figured out. - #[clap(long, short = 'e', action)] - set_exit_status: bool, - /// Always exit with success when Semantic Versions are Equal. - /// - /// Mostly impacts the output when the flag `set_exit_status` is set. - #[clap(long, short = 's', action)] - semantic_exit_status: bool, - /// The base version used for comparison. - a: Version, - /// The version we are comparing against. - b: Version, - }, - /// Sort a list of valid Semantic Versions, with either Semantic or Lexical ordering. - /// - /// Results are grouped by default, under the meaningful components of Semantic - /// Versioning (without build metadata), then enumerated under that component. - #[clap_mcp(requires = "versions")] - Sort { - #[clap(long, short = 'f', default_value = None)] - /// Only emit versions that match a filter. - /// - /// These filter rules are described by the semver crate `VersionReq`` - /// documentation, and more generally in the cargo book. - /// - /// In particular, note the warnings around pre-releases in the - /// VersionReq documentation. - /// - /// References: - /// - https://docs.rs/semver/1.0.25/semver/struct.VersionReq.html - /// - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html - filter: Option, - - #[clap(long, action)] - /// Lexical Sorting (aka Total Order). - /// - /// WARNING: This may lead to bad choices surrounding semantic - /// versioning, - /// - /// This is bound to be controversial, but worth understanding. - /// - /// Semver 2.0.0 §10 states that: - /// "Build metadata MUST be ignored when determining version - /// precedence." - /// - /// This has been set to the default behavior of emulating undefined - /// behavior, because it MUST be ignored. It is quite common, for - /// people to accidentally choose the sorting order of their favorite - /// or most familiar tool, and not the specification itself. This - /// enforces by default, the ignoring of the version precedence. - /// - /// Additionally, we must interpret the following statement as - /// undefined ordering for the case where Build Metadata may be `None` - /// or `Some`: - /// - /// "Thus two versions that differ only in the build metadata, have - /// the same precedence." - /// - /// References: - /// - https://semver.org/#spec-item-10 - lexical_sorting: bool, - - #[clap(long, short = 'r', action)] - /// Reverses ordering. - /// - /// Note, "reversing" always effects the comparable versions being - /// ordered, but is ignored when NOT lexically sorted, for the list of - /// semantically identical versions (aka, different metadata). Since by - /// default they are randomly sorted, there is no point. - reverse: bool, - - #[clap(long, action)] - /// Flatten the map, and provide a list of versions. - /// - /// WARNING: This may lead to bad choices surrounding semantic - /// versioning. - flatten: bool, - - #[clap(long, action)] - /// Fail, if potentially ambiguous precedence may emerge from these - /// versions (multiple matching M.M.P-PR, but non-matching metadata). - fail_if_potentially_ambiguous: bool, - - /// If no versions are present, then the tool will read from stdin, one - /// version per line. - versions: Option>, - }, - /// Test a Semantic Version against a filter - FilterTest { - /// Filter to test against a specific Semantic Version. - /// - /// These filter rules are described by the semver crate `VersionReq`` - /// documentation, and more generally in the cargo book. - /// - /// In particular, note the warnings around pre-releases in the - /// VersionReq documentation. - /// - /// The Status Code will be 0 if it passes, non-zero if it fails. - /// - /// References: - /// - https://docs.rs/semver/1.0.25/semver/struct.VersionReq.html - /// - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html - filter: VersionReq, - - /// Version to test - semantic_version: Version, - }, - /// Simply validates an argument, to confirm it is a valid Semantic Version - /// - /// The Status Code will be 0 if it is valid, non-zero if it is not. - Validate { - /// Version to validate - version: String, - - /// "Small" will ensure the MAJOR, MINOR & PATCH components are under [u64::MAX]. - #[clap(long, short = 's', action)] - small: bool, - }, - /// Generate random & valid Semantic Version Strings - Generate { - /// "Small" will ensure the MAJOR, MINOR & PATCH components are under [u64::MAX]. - #[clap(long, short = 's', action)] - small: bool, - - /// How many to create (default 1) - #[clap(default_value_t = 1)] - count: usize, - }, - /// Set commands will replace one segment of semver, with a specified one, - /// and print out the mutated version. - Set { - semantic_version: Version, - - #[clap(long, action)] - set_major: Option, - #[clap(long, action)] - set_minor: Option, - #[clap(long, action)] - set_patch: Option, - #[clap(long, action)] - set_pre_release: Option, - #[clap(long, action)] - set_build_metadata: Option, - }, - /// Bump commands will increment one segment of semver by the specified - /// amount, and print out the mutated version. - /// - /// Only Major, Minor and Patch are supported. You'll want to consider the - /// `set` subcommand for pre-release and build-metadata. - Bump { - semantic_version: Version, - - #[clap(long, action)] - bump_major: Option, - #[clap(long, action)] - bump_minor: Option, - #[clap(long, action)] - bump_patch: Option, - // NOTE(canardleteer): We could actually do bumps on pre-release and - // build-metadata, if we supported a selector and - // confirmed the segment chosen was numeric. - }, -} - -fn main() -> Result> { - let args = clap_mcp::parse_or_serve_mcp_attr::(); - - let cmd_clone = args.cmd.clone(); - let result = run(args.cmd)?; - - let ignore_exit_status_from_output = match (&cmd_clone, &result) { - ( - Commands::Compare { - set_exit_status, - semantic_exit_status, - .. - }, - SubcommandResult::ComparisonStatement(c), - ) => { - !set_exit_status - || (*semantic_exit_status && c.semantic_ordering() == &SerializableOrdering::Equal) - } - _ => false, - }; - - print!("{}", args.out.format_result(&result)?); - - Ok(ExitOutcome::new(result, ignore_exit_status_from_output)) -} - -/// Run a subcommand and produce a result. Used by both CLI and MCP. -fn run(cmd: Commands) -> Result { - let result: SubcommandResult = match cmd { - Commands::Explain { semantic_version } => explain(&semantic_version).into(), - Commands::Compare { - set_exit_status: _, - semantic_exit_status: _, - a, - b, - } => compare(&a, &b).into(), - Commands::Sort { - versions, - filter, - lexical_sorting, - reverse, - flatten, - fail_if_potentially_ambiguous, - } => { - let mut parsed_versions = Vec::new(); - - match versions { - Some(versions) => parsed_versions = versions, - None => { - let lines = io::stdin().lines(); - for (line_no, line) in lines.enumerate() { - match line { - Ok(line) => { - let line = line.trim(); - parsed_versions.push(Version::parse(line).map_err(|e| { - ApplicationError::InvalidArgument { - expected: format!("valid semver at line {line_no}: {line}"), - found: e.to_string(), - } - })?); - Ok(()) - } - Err(e) => Err(ApplicationError::InvalidArgument { - expected: "to be able to read from stdin".to_string(), - found: e.to_string(), - }), - }? - } - } - } - - let mut ordered_version_list = - sort(&mut parsed_versions, &filter, lexical_sorting, reverse); - - if fail_if_potentially_ambiguous && ordered_version_list.potentially_ambiguous() { - return Err(ApplicationError::FailedRequirementError { - err: "Potential Ambiguity Detected".to_string(), - }); - } - - if flatten { - FlatVersionsList::from(&mut ordered_version_list).into() - } else { - ordered_version_list.into() - } - } - Commands::FilterTest { - filter, - semantic_version, - } => filter_test(&filter, &semantic_version).into(), - Commands::Validate { version, small } => validate(version, small).into(), - Commands::Generate { small, count } => generate(small, count).into(), - Commands::Set { - semantic_version, - set_major, - set_minor, - set_patch, - set_pre_release, - set_build_metadata, - } => set( - &semantic_version, - set_major, - set_minor, - set_patch, - set_pre_release, - set_build_metadata, - ) - .map_err(|e| ApplicationError::InvalidArgument { - expected: "valid set operation".to_string(), - found: e.to_string(), - })? - .into(), - Commands::Bump { - semantic_version, - bump_major, - bump_minor, - bump_patch, - } => bump(&semantic_version, bump_major, bump_minor, bump_patch) - .map_err(|e| ApplicationError::InvalidArgument { - expected: "valid bump operation".to_string(), - found: e.to_string(), - })? - .into(), - }; - Ok(result) -} - -fn sort( - versions: &mut Vec, - filter: &Option, - lexical_sorting: bool, - reverse: bool, -) -> OrderedVersionMap { - OrderedVersionMap::new(versions, filter, lexical_sorting, reverse) -} - -/// Returns the semantic and lexical equivalence of 2 versions. -fn compare(a: &Version, b: &Version) -> ComparisonStatement { - ComparisonStatement::new(a, b) -} - -fn explain(v: &Version) -> VersionExplanation { - VersionExplanation::from(v) -} - -fn filter_test(filter: &VersionReq, semantic_version: &Version) -> FilterTestResult { - FilterTestResult::filter_test(filter, semantic_version) -} - -fn validate(semantic_version: String, small: bool) -> ValidateResult { - // NOTE(canardleteer): This is somewhat of a useless code path. - ValidateResult::validate(semantic_version, small) -} - -fn generate(small: bool, count: usize) -> GenerateResult { - GenerateResult::new(small, count) -} - -fn set( - version: &Version, - major: Option, - minor: Option, - patch: Option, - pre_release: Option, - build_metadata: Option, -) -> Result> { - VersionMutationResult::set(version, major, minor, patch, pre_release, build_metadata) -} - -fn bump( - version: &Version, - major: Option, - minor: Option, - patch: Option, -) -> Result> { - VersionMutationResult::bump(version, major, minor, patch) -} - -#[cfg(test)] -mod tests { - use proptest::prelude::*; - use proptest_semver::*; - - use crate::{SerializableOrdering, version_without_build_metadata}; - - proptest! { - // None of these tests do much more than ensure the - // application doesn't bounce back or crash on valid - // input. - #[test] - fn compare(a in arb_version(), b in arb_version()) { - let comparison = super::compare(&a, &b); - - let a_no_build = version_without_build_metadata(&a); - let b_no_build = version_without_build_metadata(&b); - - match a.cmp(&b) { - std::cmp::Ordering::Equal => { - prop_assert_eq!(a_no_build, b_no_build); - prop_assert!(comparison.semantic_ordering() == &SerializableOrdering::Equal); - }, - std::cmp::Ordering::Greater => { - prop_assert!(comparison.lexical_ordering() == &SerializableOrdering::Greater || comparison.lexical_ordering() == &SerializableOrdering::Equal); - prop_assert_eq!(comparison.semantic_ordering(), &SerializableOrdering::Greater); - }, - std::cmp::Ordering::Less => { - prop_assert!(comparison.lexical_ordering() == &SerializableOrdering::Less || comparison.lexical_ordering() == &SerializableOrdering::Equal); - prop_assert_eq!(comparison.semantic_ordering(), &SerializableOrdering::Less); - }, - - }; - } - - #[test] - fn explain(version in arb_version()) { - super::explain(&version); - } - - #[test] - fn validate(version in arb_semver(), small: bool) { - super::validate(version, small); - } - - #[test] - fn filter_test(filter in arb_version_req(MAX_COMPARATORS_IN_VERSION_REQ_STRING), version in arb_version()) { - super::filter_test(&filter, &version); - } - - #[test] - fn sort(versions in arb_vec_versions(256), filter in arb_optional_version_req(0.5, MAX_COMPARATORS_IN_VERSION_REQ_STRING), lexical_sorting in any::(), reverse in any::()) { - let mut versions = versions.clone(); - super::sort(&mut versions, &filter, lexical_sorting, reverse); - } - - #[test] - fn generate(small: bool, count: u8) { - // Not going to flex maxing out memory allocations here, limiting - // to u8 testing. - super::generate(small, count.into()); +fn main() -> std::process::ExitCode { + match sem_tool::run_app() { + Ok(exit_code) => exit_code, + Err(e) => { + eprintln!("{e}"); + std::process::ExitCode::FAILURE } } } diff --git a/src/misc.rs b/src/misc.rs index 4eb5864..663bc04 100644 --- a/src/misc.rs +++ b/src/misc.rs @@ -15,17 +15,19 @@ //! //! This is a bunch of last mile display + serialization logic. use clap::ValueEnum; +#[cfg(feature = "mcp")] use clap_mcp::{ClapMcpToolError, ClapMcpToolOutput, IntoClapMcpResult, IntoClapMcpToolError}; use core::fmt; -use serde::Serialize; +#[cfg(feature = "mcp")] use schemars::JsonSchema; +use serde::Serialize; use std::process::{ExitCode, Termination}; use thiserror::Error; use crate::results; #[derive(Error, Debug)] -pub(crate) enum ApplicationError { +pub enum ApplicationError { /// We got invalid input. #[error("Invalid input (expected {expected:?}, got {found:?}")] InvalidArgument { expected: String, found: String }, @@ -40,7 +42,7 @@ pub(crate) enum ApplicationError { } #[derive(ValueEnum, Clone, Debug)] -pub(crate) enum OutputFormat { +pub enum OutputFormat { Text, Yaml, Json, @@ -95,7 +97,8 @@ impl Termination for ExitOutcome { } } -#[derive(Serialize, JsonSchema)] +#[derive(Serialize)] +#[cfg_attr(feature = "mcp", derive(JsonSchema))] #[serde(untagged)] pub(crate) enum SubcommandResult { /// Assertion by this program @@ -164,6 +167,7 @@ impl From for SubcommandResult { } /// MCP tool response wrapper: explicit `ok` for exit-code-like semantics (Validate, FilterTest, Compare). +#[cfg(feature = "mcp")] #[derive(Serialize, JsonSchema)] pub(crate) struct McpToolOutput { pub(crate) ok: bool, @@ -171,6 +175,7 @@ pub(crate) struct McpToolOutput { } /// Compute success for MCP: true for exit-code success, so clients get explicit semantics. +#[cfg(feature = "mcp")] fn success_for_mcp(result: &SubcommandResult) -> bool { match result { SubcommandResult::ValidateResult(v) => v.success(), @@ -180,6 +185,7 @@ fn success_for_mcp(result: &SubcommandResult) -> bool { } } +#[cfg(feature = "mcp")] impl IntoClapMcpResult for SubcommandResult { fn into_tool_result(self) -> std::result::Result { let ok = success_for_mcp(&self); @@ -190,6 +196,7 @@ impl IntoClapMcpResult for SubcommandResult { } } +#[cfg(feature = "mcp")] impl IntoClapMcpToolError for ApplicationError { fn into_tool_error(self) -> ClapMcpToolError { ClapMcpToolError::text(self.to_string()) diff --git a/src/results.rs b/src/results.rs index 5a4efa7..adf03e6 100644 --- a/src/results.rs +++ b/src/results.rs @@ -24,14 +24,16 @@ use std::{ use indexmap::IndexMap; use rand::prelude::*; use regex::Regex; +#[cfg(feature = "mcp")] +use schemars::JsonSchema; use semver::{BuildMetadata, Prerelease, Version, VersionReq}; use serde::Serialize; -use schemars::JsonSchema; use super::regex::{generate_any_valid_semver, generate_u64_safe_semver}; /// The result of a simple filter test. -#[derive(Serialize, JsonSchema, PartialEq)] +#[derive(Serialize, PartialEq)] +#[cfg_attr(feature = "mcp", derive(JsonSchema))] pub(crate) struct ValidateResult { valid: bool, } @@ -51,6 +53,7 @@ impl ValidateResult { } /// True if validation passed (exit-code success). Used for MCP explicit success wrapper. + #[cfg_attr(not(feature = "mcp"), allow(dead_code))] pub(crate) fn success(&self) -> bool { self.valid } @@ -77,7 +80,8 @@ impl Termination for ValidateResult { } /// The result of a simple filter test. -#[derive(Serialize, JsonSchema, PartialEq)] +#[derive(Serialize, PartialEq)] +#[cfg_attr(feature = "mcp", derive(JsonSchema))] pub(crate) struct FilterTestResult { pass: bool, } @@ -88,6 +92,7 @@ impl FilterTestResult { } /// True if filter matched (exit-code success). Used for MCP explicit success wrapper. + #[cfg_attr(not(feature = "mcp"), allow(dead_code))] pub(crate) fn success(&self) -> bool { self.pass } @@ -119,7 +124,8 @@ impl fmt::Display for FilterTestResult { } } -#[derive(Clone, Debug, Serialize, JsonSchema, PartialEq)] +#[derive(Clone, Debug, Serialize, PartialEq)] +#[cfg_attr(feature = "mcp", derive(JsonSchema))] pub(crate) enum SegmentType { Numeric, Ascii, @@ -137,7 +143,8 @@ impl fmt::Display for SegmentType { /// Describes a dot separated segment of either a Pre-Release, or Build Metadata string. /// /// Kind describes how the value is meant to be interpreted for precedence. -#[derive(Clone, Debug, Serialize, JsonSchema, PartialEq)] +#[derive(Clone, Debug, Serialize, PartialEq)] +#[cfg_attr(feature = "mcp", derive(JsonSchema))] pub(crate) struct PreMetaSegment { kind: SegmentType, value: String, @@ -164,7 +171,8 @@ impl From<&str> for PreMetaSegment { } /// Descriptive information about a Version. -#[derive(Clone, Serialize, JsonSchema, PartialEq)] +#[derive(Clone, Serialize, PartialEq)] +#[cfg_attr(feature = "mcp", derive(JsonSchema))] pub(crate) struct VersionExplanation { major: u64, minor: u64, @@ -254,9 +262,10 @@ impl Termination for VersionExplanation { } /// A simple list of Versions. -#[derive(Serialize, JsonSchema, PartialEq)] +#[derive(Serialize, PartialEq)] +#[cfg_attr(feature = "mcp", derive(JsonSchema))] pub(crate) struct FlatVersionsList { - #[schemars(with = "Vec")] + #[cfg_attr(feature = "mcp", schemars(with = "Vec"))] versions: Vec, potentially_ambiguous: bool, } @@ -291,7 +300,8 @@ impl Termination for FlatVersionsList { /// A simple list of Strings. /// /// NOTE(canardleteer): Probably could become any serializable type with Display. -#[derive(Serialize, JsonSchema, PartialEq)] +#[derive(Serialize, PartialEq)] +#[cfg_attr(feature = "mcp", derive(JsonSchema))] pub(crate) struct FlatStringList { versions: Vec, } @@ -320,9 +330,16 @@ impl Termination for FlatStringList { } /// A usefully ordered list of versions. -#[derive(Serialize, JsonSchema)] +#[derive(Serialize)] +#[cfg_attr(feature = "mcp", derive(JsonSchema))] pub(crate) struct OrderedVersionMap { - #[schemars(rename = "versions", with = "std::collections::HashMap>")] + #[cfg_attr( + feature = "mcp", + schemars( + rename = "versions", + with = "std::collections::HashMap>" + ) + )] #[serde(rename(serialize = "versions"))] inner: IndexMap>, potentially_ambiguous: bool, @@ -416,7 +433,8 @@ impl Termination for OrderedVersionMap { } /// A statement about the comparison about 2 versions -#[derive(Serialize, JsonSchema, PartialEq)] +#[derive(Serialize, PartialEq)] +#[cfg_attr(feature = "mcp", derive(JsonSchema))] pub(crate) struct ComparisonStatement { semantic_ordering: SerializableOrdering, lexical_ordering: SerializableOrdering, @@ -443,13 +461,15 @@ impl ComparisonStatement { } /// True when both semantic and lexical orderings are Equal (exit-code success). Used for MCP explicit success wrapper. + #[cfg_attr(not(feature = "mcp"), allow(dead_code))] pub(crate) fn both_equal(&self) -> bool { *self.semantic_ordering() == SerializableOrdering::Equal && *self.lexical_ordering() == SerializableOrdering::Equal } } -#[derive(Serialize, JsonSchema, PartialEq)] +#[derive(Serialize, PartialEq)] +#[cfg_attr(feature = "mcp", derive(JsonSchema))] pub(crate) struct GenerateResult { inner: Vec, } @@ -478,9 +498,10 @@ impl fmt::Display for GenerateResult { } } -#[derive(Serialize, JsonSchema, PartialEq)] +#[derive(Serialize, PartialEq)] +#[cfg_attr(feature = "mcp", derive(JsonSchema))] pub(crate) struct VersionMutationResult { - #[schemars(with = "String")] + #[cfg_attr(feature = "mcp", schemars(with = "String"))] pub(crate) mutated_version: Version, } @@ -645,8 +666,9 @@ impl fmt::Display for ComparisonStatement { } /// Just a small reimplementation of std::Ordering with Serialization. -#[derive(Debug, Serialize, JsonSchema, PartialEq)] -pub(crate) enum SerializableOrdering { +#[derive(Debug, Serialize, PartialEq)] +#[cfg_attr(feature = "mcp", derive(JsonSchema))] +pub enum SerializableOrdering { Less, Greater, Equal, @@ -672,7 +694,7 @@ impl From for SerializableOrdering { } } -pub(crate) fn version_without_build_metadata(version: &Version) -> Version { +pub fn version_without_build_metadata(version: &Version) -> Version { Version { major: version.major, minor: version.minor, diff --git a/tests/cli_insta.rs b/tests/cli_insta.rs index 8f684e9..e9c3fb2 100644 --- a/tests/cli_insta.rs +++ b/tests/cli_insta.rs @@ -22,11 +22,32 @@ fn cli() -> Command { Command::new(get_cargo_bin("sem-tool")) } +/// When set (e.g. in CI for the mcp feature job), skip the default insta test so we don't +/// compare against default-feature snapshots. The mcp binary has different --help (e.g. --mcp, +/// --export-skills) and possibly other divergence. +fn skip_default_insta() -> bool { + std::env::var("SEM_TOOL_SKIP_INSTA").unwrap_or_default() == "1" +} + #[test] fn cli_insta() { + if skip_default_insta() { + return; + } // Giant map of various tests for insta. let mut insta_targets = HashMap::new(); + // --help for root and each subcommand (snapshot CLI help text) + insta_targets.insert("help.root", vec!["--help"]); + insta_targets.insert("help.explain", vec![COMMAND_EXPLAIN, "--help"]); + insta_targets.insert("help.compare", vec![COMMAND_COMPARE, "--help"]); + insta_targets.insert("help.sort", vec![COMMAND_SORT, "--help"]); + insta_targets.insert("help.filter-test", vec![COMMAND_FILTER_TEST, "--help"]); + insta_targets.insert("help.validate", vec![COMMAND_VALIDATE, "--help"]); + insta_targets.insert("help.generate", vec![COMMAND_GENERATE, "--help"]); + insta_targets.insert("help.set", vec![COMMAND_SET, "--help"]); + insta_targets.insert("help.bump", vec![COMMAND_BUMP, "--help"]); + // Filter Tests insta_targets.insert( "filter.invalid-semver.1", @@ -188,6 +209,42 @@ fn cli_insta() { insta_targets.insert("set.simple.2", vec![COMMAND_SET, "1.1.1", "--set-major=20"]); for (key, args) in insta_targets.iter() { - assert_cmd_snapshot!(*key, cli().args(args)); + insta::with_settings!({ + filters => vec![ + (r"sem-tool\.exe", "sem-tool"), + ] + }, { + assert_cmd_snapshot!(*key, cli().args(args)); + }); + } +} + +/// Snapshots --help output for the MCP-enabled binary. Run with `SEM_TOOL_MCP_HELP=1` when +/// testing the `mcp` feature build so we have separate snapshots (mcp adds --mcp, --export-skills, +/// etc.). In CI the mcp job sets both SEM_TOOL_SKIP_INSTA and SEM_TOOL_MCP_HELP. +#[test] +fn cli_insta_mcp_help() { + if std::env::var("SEM_TOOL_MCP_HELP").unwrap_or_default() != "1" { + return; + } + let help_cases = [ + ("help_mcp.root", vec!["--help"]), + ("help_mcp.explain", vec![COMMAND_EXPLAIN, "--help"]), + ("help_mcp.compare", vec![COMMAND_COMPARE, "--help"]), + ("help_mcp.sort", vec![COMMAND_SORT, "--help"]), + ("help_mcp.filter-test", vec![COMMAND_FILTER_TEST, "--help"]), + ("help_mcp.validate", vec![COMMAND_VALIDATE, "--help"]), + ("help_mcp.generate", vec![COMMAND_GENERATE, "--help"]), + ("help_mcp.set", vec![COMMAND_SET, "--help"]), + ("help_mcp.bump", vec![COMMAND_BUMP, "--help"]), + ]; + for (name, args) in help_cases { + insta::with_settings!({ + filters => vec![ + (r"sem-tool\.exe", "sem-tool"), + ] + }, { + assert_cmd_snapshot!(name, cli().args(args)); + }); } } diff --git a/tests/snapshots/cli_insta__help.bump.snap b/tests/snapshots/cli_insta__help.bump.snap new file mode 100644 index 0000000..24460d1 --- /dev/null +++ b/tests/snapshots/cli_insta__help.bump.snap @@ -0,0 +1,35 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - bump + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Bump commands will increment one segment of semver by the specified amount, and print out the mutated version. + +Only Major, Minor and Patch are supported. You'll want to consider the `set` subcommand for pre-release and build-metadata. + +Usage: sem-tool bump [OPTIONS] + +Arguments: + + + +Options: + --bump-major + + + --bump-minor + + + --bump-patch + + + -h, --help + Print help (see a summary with '-h') + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help.compare.snap b/tests/snapshots/cli_insta__help.compare.snap new file mode 100644 index 0000000..11f7e98 --- /dev/null +++ b/tests/snapshots/cli_insta__help.compare.snap @@ -0,0 +1,49 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - compare + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Compare 2 Semantic Versions. + +Results are provided in the form "A is {Greater,Equals,Less} {to,than} B", with both Semantic results (meaningful results under Semantic Versioning), as well as Lexical results (meaningless, but handy for sorting text lists). + +Without enabling `--set_exit_status`, the exit status is generally meaningless, other than confirming that the arguments were valid. + +Usage: sem-tool compare [OPTIONS] + +Arguments: + + The base version used for comparison + + + The version we are comparing against + +Options: + -e, --set-exit-status + If you want some slightly complex exit status codes for this dual compare, you can turn them on with this flag. + + When both Semantic and Lexical comparisons are Equal, the command will end with an exit status of 0 (Success). + + All other outcomes, are returned with an exit status of the form: 1XY [between 100-122]. + + - With X being (0 if Less, 1 if Equal, 2 if Greater) on the Semantic Compare + + - With Y being (0 if Less, 1 if Equal, 2 if Greater) on the Lexical Compare + + The non-0 exit status codes, should be considered UNSTABLE, because something better can probably be figured out. + + -s, --semantic-exit-status + Always exit with success when Semantic Versions are Equal. + + Mostly impacts the output when the flag `set_exit_status` is set. + + -h, --help + Print help (see a summary with '-h') + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help.explain.snap b/tests/snapshots/cli_insta__help.explain.snap new file mode 100644 index 0000000..5c9b505 --- /dev/null +++ b/tests/snapshots/cli_insta__help.explain.snap @@ -0,0 +1,32 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - explain + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Explain a valid Semantic Version as parsed by the spec. + +Breaks apart the Semantic Version, into it's individual components. + +All values are returned as strings, because the unsigned integer types are not necessarily bound by a numeric type that is parsable by common libraries. + +It is worth noting, Semver 2.0.0 §11.4.1 & §11.4.2 pre-release & metadata dot separated values, cannot be negative numbers, since they cannot be represented with hyphens. + +Reference: https://semver.org/#spec-item-11 + +Usage: sem-tool explain + +Arguments: + + + +Options: + -h, --help + Print help (see a summary with '-h') + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help.filter-test.snap b/tests/snapshots/cli_insta__help.filter-test.snap new file mode 100644 index 0000000..fc306dd --- /dev/null +++ b/tests/snapshots/cli_insta__help.filter-test.snap @@ -0,0 +1,35 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - filter-test + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Test a Semantic Version against a filter + +Usage: sem-tool filter-test + +Arguments: + + Filter to test against a specific Semantic Version. + + These filter rules are described by the semver crate `VersionReq` documentation, and more generally in the cargo book. + + In particular, note the warnings around pre-releases in the VersionReq documentation. + + The Status Code will be 0 if it passes, non-zero if it fails. + + References: - https://docs.rs/semver/1.0.25/semver/struct.VersionReq.html - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html + + + Version to test + +Options: + -h, --help + Print help (see a summary with '-h') + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help.generate.snap b/tests/snapshots/cli_insta__help.generate.snap new file mode 100644 index 0000000..741dce1 --- /dev/null +++ b/tests/snapshots/cli_insta__help.generate.snap @@ -0,0 +1,23 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - generate + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Generate random & valid Semantic Version Strings. They may be large and awkward, but valid Semantic Versions. Useful for testing and validation + +Usage: sem-tool generate [OPTIONS] [COUNT] + +Arguments: + [COUNT] How many to create (default 1) [default: 1] + +Options: + -s, --small "Small" will ensure the MAJOR, MINOR & PATCH components are under [u64::MAX] + -h, --help Print help + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help.root.snap b/tests/snapshots/cli_insta__help.root.snap new file mode 100644 index 0000000..cf935de --- /dev/null +++ b/tests/snapshots/cli_insta__help.root.snap @@ -0,0 +1,31 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +A simple Semantic Versioning CLI tool for parsing, filtering, sorting and explaining + +Usage: sem-tool [OPTIONS] + +Commands: + explain Explain a valid Semantic Version as parsed by the spec + compare Compare 2 Semantic Versions + sort Sort a list of valid Semantic Versions, with either Semantic or Lexical ordering + filter-test Test a Semantic Version against a filter + validate Simply validates an argument, to confirm it is a valid Semantic Version + generate Generate random & valid Semantic Version Strings. They may be large and awkward, but valid Semantic Versions. Useful for testing and validation + set Set commands will replace one segment of semver, with a specified one, and print out the mutated version + bump Bump commands will increment one segment of semver by the specified amount, and print out the mutated version + help Print this message or the help of the given subcommand(s) + +Options: + -o, --out Output format (yaml, text, json) [default: yaml] [possible values: text, yaml, json] + -h, --help Print help + -V, --version Print version + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help.set.snap b/tests/snapshots/cli_insta__help.set.snap new file mode 100644 index 0000000..ffa1d92 --- /dev/null +++ b/tests/snapshots/cli_insta__help.set.snap @@ -0,0 +1,27 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - set + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Set commands will replace one segment of semver, with a specified one, and print out the mutated version + +Usage: sem-tool set [OPTIONS] + +Arguments: + + +Options: + --set-major + --set-minor + --set-patch + --set-pre-release + --set-build-metadata + -h, --help Print help + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help.sort.snap b/tests/snapshots/cli_insta__help.sort.snap new file mode 100644 index 0000000..a35a7e5 --- /dev/null +++ b/tests/snapshots/cli_insta__help.sort.snap @@ -0,0 +1,63 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - sort + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Sort a list of valid Semantic Versions, with either Semantic or Lexical ordering. + +Results are grouped by default, under the meaningful components of Semantic Versioning (without build metadata), then enumerated under that component. + +Usage: sem-tool sort [OPTIONS] [VERSIONS]... + +Arguments: + [VERSIONS]... + If no versions are present, then the tool will read from stdin, one version per line + +Options: + -f, --filter + Only emit versions that match a filter. + + These filter rules are described by the semver crate `VersionReq` documentation, and more generally in the cargo book. + + In particular, note the warnings around pre-releases in the VersionReq documentation. + + References: - https://docs.rs/semver/1.0.25/semver/struct.VersionReq.html - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html + + --lexical-sorting + Lexical Sorting (aka Total Order). + + WARNING: This may lead to bad choices surrounding semantic versioning. + + Semver 2.0.0 §10 states that: "Build metadata MUST be ignored when determining version precedence." + + This has been set to the default behavior of emulating undefined behavior, because it MUST be ignored. It is quite common, for people to accidentally choose the sorting order of their favorite or most familiar tool, and not the specification itself. + + Additionally, we must interpret the following statement as undefined ordering for the case where Build Metadata may be `None` or `Some`: + + "Thus two versions that differ only in the build metadata, have the same precedence." + + References: - https://semver.org/#spec-item-10 + + -r, --reverse + Reverses ordering. + + Note, "reversing" always effects the comparable versions being ordered, but is ignored when NOT lexically sorted, for the list of semantically identical versions (aka, different metadata). Since by default they are randomly sorted, there is no point. + + --flatten + Flatten the map, and provide a list of versions. + + WARNING: This may lead to bad choices surrounding semantic versioning. + + --fail-if-potentially-ambiguous + Fail, if potentially ambiguous precedence may emerge from these versions (multiple matching M.M.P-PR, but non-matching metadata) + + -h, --help + Print help (see a summary with '-h') + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help.validate.snap b/tests/snapshots/cli_insta__help.validate.snap new file mode 100644 index 0000000..8e39bdf --- /dev/null +++ b/tests/snapshots/cli_insta__help.validate.snap @@ -0,0 +1,29 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - validate + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Simply validates an argument, to confirm it is a valid Semantic Version + +The Status Code will be 0 if it is valid, non-zero if it is not. + +Usage: sem-tool validate [OPTIONS] + +Arguments: + + Version to validate + +Options: + -s, --small + "Small" will ensure the MAJOR, MINOR & PATCH components are under [u64::MAX] + + -h, --help + Print help (see a summary with '-h') + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help_mcp.bump.snap b/tests/snapshots/cli_insta__help_mcp.bump.snap new file mode 100644 index 0000000..851dc06 --- /dev/null +++ b/tests/snapshots/cli_insta__help_mcp.bump.snap @@ -0,0 +1,41 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - bump + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Bump commands will increment one segment of semver by the specified amount, and print out the mutated version. + +Only Major, Minor and Patch are supported. You'll want to consider the `set` subcommand for pre-release and build-metadata. + +Usage: sem-tool bump [OPTIONS] + +Arguments: + + + +Options: + --bump-major + + + --bump-minor + + + --bump-patch + + + --mcp + Run an MCP server over stdio that exposes this CLI's clap schema + + --export-skills + Generate Agent Skills (SKILL.md) from tools, resources, and prompts, then exit + + -h, --help + Print help (see a summary with '-h') + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help_mcp.compare.snap b/tests/snapshots/cli_insta__help_mcp.compare.snap new file mode 100644 index 0000000..39c5a2b --- /dev/null +++ b/tests/snapshots/cli_insta__help_mcp.compare.snap @@ -0,0 +1,55 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - compare + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Compare 2 Semantic Versions. + +Results are provided in the form "A is {Greater,Equals,Less} {to,than} B", with both Semantic results (meaningful results under Semantic Versioning), as well as Lexical results (meaningless, but handy for sorting text lists). + +Without enabling `--set_exit_status`, the exit status is generally meaningless, other than confirming that the arguments were valid. + +Usage: sem-tool compare [OPTIONS] + +Arguments: + + The base version used for comparison + + + The version we are comparing against + +Options: + -e, --set-exit-status + If you want some slightly complex exit status codes for this dual compare, you can turn them on with this flag. + + When both Semantic and Lexical comparisons are Equal, the command will end with an exit status of 0 (Success). + + All other outcomes, are returned with an exit status of the form: 1XY [between 100-122]. + + - With X being (0 if Less, 1 if Equal, 2 if Greater) on the Semantic Compare + + - With Y being (0 if Less, 1 if Equal, 2 if Greater) on the Lexical Compare + + The non-0 exit status codes, should be considered UNSTABLE, because something better can probably be figured out. + + -s, --semantic-exit-status + Always exit with success when Semantic Versions are Equal. + + Mostly impacts the output when the flag `set_exit_status` is set. + + --mcp + Run an MCP server over stdio that exposes this CLI's clap schema + + --export-skills + Generate Agent Skills (SKILL.md) from tools, resources, and prompts, then exit + + -h, --help + Print help (see a summary with '-h') + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help_mcp.explain.snap b/tests/snapshots/cli_insta__help_mcp.explain.snap new file mode 100644 index 0000000..e46f86a --- /dev/null +++ b/tests/snapshots/cli_insta__help_mcp.explain.snap @@ -0,0 +1,38 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - explain + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Explain a valid Semantic Version as parsed by the spec. + +Breaks apart the Semantic Version, into it's individual components. + +All values are returned as strings, because the unsigned integer types are not necessarily bound by a numeric type that is parsable by common libraries. + +It is worth noting, Semver 2.0.0 §11.4.1 & §11.4.2 pre-release & metadata dot separated values, cannot be negative numbers, since they cannot be represented with hyphens. + +Reference: https://semver.org/#spec-item-11 + +Usage: sem-tool explain [OPTIONS] + +Arguments: + + + +Options: + --mcp + Run an MCP server over stdio that exposes this CLI's clap schema + + --export-skills + Generate Agent Skills (SKILL.md) from tools, resources, and prompts, then exit + + -h, --help + Print help (see a summary with '-h') + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help_mcp.filter-test.snap b/tests/snapshots/cli_insta__help_mcp.filter-test.snap new file mode 100644 index 0000000..123fd71 --- /dev/null +++ b/tests/snapshots/cli_insta__help_mcp.filter-test.snap @@ -0,0 +1,41 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - filter-test + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Test a Semantic Version against a filter + +Usage: sem-tool filter-test [OPTIONS] + +Arguments: + + Filter to test against a specific Semantic Version. + + These filter rules are described by the semver crate `VersionReq` documentation, and more generally in the cargo book. + + In particular, note the warnings around pre-releases in the VersionReq documentation. + + The Status Code will be 0 if it passes, non-zero if it fails. + + References: - https://docs.rs/semver/1.0.25/semver/struct.VersionReq.html - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html + + + Version to test + +Options: + --mcp + Run an MCP server over stdio that exposes this CLI's clap schema + + --export-skills + Generate Agent Skills (SKILL.md) from tools, resources, and prompts, then exit + + -h, --help + Print help (see a summary with '-h') + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help_mcp.generate.snap b/tests/snapshots/cli_insta__help_mcp.generate.snap new file mode 100644 index 0000000..c94718b --- /dev/null +++ b/tests/snapshots/cli_insta__help_mcp.generate.snap @@ -0,0 +1,25 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - generate + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Generate random & valid Semantic Version Strings. They may be large and awkward, but valid Semantic Versions. Useful for testing and validation + +Usage: sem-tool generate [OPTIONS] [COUNT] + +Arguments: + [COUNT] How many to create (default 1) [default: 1] + +Options: + -s, --small "Small" will ensure the MAJOR, MINOR & PATCH components are under [u64::MAX] + --mcp Run an MCP server over stdio that exposes this CLI's clap schema + --export-skills Generate Agent Skills (SKILL.md) from tools, resources, and prompts, then exit + -h, --help Print help + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help_mcp.root.snap b/tests/snapshots/cli_insta__help_mcp.root.snap new file mode 100644 index 0000000..28c1b4e --- /dev/null +++ b/tests/snapshots/cli_insta__help_mcp.root.snap @@ -0,0 +1,33 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +A simple Semantic Versioning CLI tool for parsing, filtering, sorting and explaining + +Usage: sem-tool [OPTIONS] + +Commands: + explain Explain a valid Semantic Version as parsed by the spec + compare Compare 2 Semantic Versions + sort Sort a list of valid Semantic Versions, with either Semantic or Lexical ordering + filter-test Test a Semantic Version against a filter + validate Simply validates an argument, to confirm it is a valid Semantic Version + generate Generate random & valid Semantic Version Strings. They may be large and awkward, but valid Semantic Versions. Useful for testing and validation + set Set commands will replace one segment of semver, with a specified one, and print out the mutated version + bump Bump commands will increment one segment of semver by the specified amount, and print out the mutated version + help Print this message or the help of the given subcommand(s) + +Options: + -o, --out Output format (yaml, text, json) [default: yaml] [possible values: text, yaml, json] + --mcp Run an MCP server over stdio that exposes this CLI's clap schema + --export-skills Generate Agent Skills (SKILL.md) from tools, resources, and prompts, then exit + -h, --help Print help + -V, --version Print version + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help_mcp.set.snap b/tests/snapshots/cli_insta__help_mcp.set.snap new file mode 100644 index 0000000..3964363 --- /dev/null +++ b/tests/snapshots/cli_insta__help_mcp.set.snap @@ -0,0 +1,37 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - set + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Set commands will replace one segment of semver, with a specified one, and print out the mutated version + +Usage: sem-tool set [OPTIONS] + +Arguments: + + +Options: + --set-major + + --set-minor + + --set-patch + + --set-pre-release + + --set-build-metadata + + --mcp + Run an MCP server over stdio that exposes this CLI's clap schema + --export-skills + Generate Agent Skills (SKILL.md) from tools, resources, and prompts, then exit + -h, --help + Print help + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help_mcp.sort.snap b/tests/snapshots/cli_insta__help_mcp.sort.snap new file mode 100644 index 0000000..02c95ec --- /dev/null +++ b/tests/snapshots/cli_insta__help_mcp.sort.snap @@ -0,0 +1,69 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - sort + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Sort a list of valid Semantic Versions, with either Semantic or Lexical ordering. + +Results are grouped by default, under the meaningful components of Semantic Versioning (without build metadata), then enumerated under that component. + +Usage: sem-tool sort [OPTIONS] [VERSIONS]... + +Arguments: + [VERSIONS]... + If no versions are present, then the tool will read from stdin, one version per line + +Options: + -f, --filter + Only emit versions that match a filter. + + These filter rules are described by the semver crate `VersionReq` documentation, and more generally in the cargo book. + + In particular, note the warnings around pre-releases in the VersionReq documentation. + + References: - https://docs.rs/semver/1.0.25/semver/struct.VersionReq.html - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html + + --lexical-sorting + Lexical Sorting (aka Total Order). + + WARNING: This may lead to bad choices surrounding semantic versioning. + + Semver 2.0.0 §10 states that: "Build metadata MUST be ignored when determining version precedence." + + This has been set to the default behavior of emulating undefined behavior, because it MUST be ignored. It is quite common, for people to accidentally choose the sorting order of their favorite or most familiar tool, and not the specification itself. + + Additionally, we must interpret the following statement as undefined ordering for the case where Build Metadata may be `None` or `Some`: + + "Thus two versions that differ only in the build metadata, have the same precedence." + + References: - https://semver.org/#spec-item-10 + + -r, --reverse + Reverses ordering. + + Note, "reversing" always effects the comparable versions being ordered, but is ignored when NOT lexically sorted, for the list of semantically identical versions (aka, different metadata). Since by default they are randomly sorted, there is no point. + + --flatten + Flatten the map, and provide a list of versions. + + WARNING: This may lead to bad choices surrounding semantic versioning. + + --fail-if-potentially-ambiguous + Fail, if potentially ambiguous precedence may emerge from these versions (multiple matching M.M.P-PR, but non-matching metadata) + + --mcp + Run an MCP server over stdio that exposes this CLI's clap schema + + --export-skills + Generate Agent Skills (SKILL.md) from tools, resources, and prompts, then exit + + -h, --help + Print help (see a summary with '-h') + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__help_mcp.validate.snap b/tests/snapshots/cli_insta__help_mcp.validate.snap new file mode 100644 index 0000000..ff1626f --- /dev/null +++ b/tests/snapshots/cli_insta__help_mcp.validate.snap @@ -0,0 +1,35 @@ +--- +source: tests/cli_insta.rs +info: + program: sem-tool + args: + - validate + - "--help" +--- +success: true +exit_code: 0 +----- stdout ----- +Simply validates an argument, to confirm it is a valid Semantic Version + +The Status Code will be 0 if it is valid, non-zero if it is not. + +Usage: sem-tool validate [OPTIONS] + +Arguments: + + Version to validate + +Options: + -s, --small + "Small" will ensure the MAJOR, MINOR & PATCH components are under [u64::MAX] + + --mcp + Run an MCP server over stdio that exposes this CLI's clap schema + + --export-skills + Generate Agent Skills (SKILL.md) from tools, resources, and prompts, then exit + + -h, --help + Print help (see a summary with '-h') + +----- stderr ----- diff --git a/tests/snapshots/cli_insta__sort.complex.1.snap b/tests/snapshots/cli_insta__sort.complex.1.snap index aa15940..393bb66 100644 --- a/tests/snapshots/cli_insta__sort.complex.1.snap +++ b/tests/snapshots/cli_insta__sort.complex.1.snap @@ -14,4 +14,4 @@ exit_code: 1 ----- stdout ----- ----- stderr ----- -Error: FailedRequirementError { err: "Potential Ambiguity Detected" } +Failed a requirement "Potential Ambiguity Detected"