From 7b5ba4797b122f323a119bed22c6db9178a46ea5 Mon Sep 17 00:00:00 2001 From: Hongda Chen <54146249+Adancurusul@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:18:46 +0800 Subject: [PATCH 1/8] chore: harden release checks and add cli skill workflow --- .github/scripts/validate_skill.py | 52 + .github/workflows/ci.yml | 62 + Cargo.lock | 1004 +++++----- Cargo.toml | 4 +- README.md | 346 ++-- README_zh.md | 310 ++- examples/README.md | 33 +- examples/STM32_demo/.cargo/config.toml | 1 - examples/STM32_demo/Cargo.lock | 327 ++-- examples/STM32_demo/Cargo.toml | 4 + examples/STM32_demo/README.md | 140 +- examples/STM32_demo/rust-toolchain.toml | 2 +- skills/embedded-debugger/SKILL.md | 64 + skills/embedded-debugger/agents/openai.yaml | 4 + .../references/default-prompt.md | 13 + src/config.rs | 224 ++- src/debugger/discovery.rs | 120 +- src/debugger/mod.rs | 2 +- src/error.rs | 2 +- src/flash/manager.rs | 142 +- src/flash/mod.rs | 10 +- src/lib.rs | 14 +- src/main.rs | 220 ++- src/rtt/elf_parser.rs | 83 +- src/rtt/manager.rs | 221 ++- src/rtt/mod.rs | 8 +- src/tools/debugger_tools.rs | 1737 +++++++++++------ src/tools/mod.rs | 4 +- src/tools/types.rs | 52 +- src/utils.rs | 2 +- tests/integration_tests.rs | 26 +- 31 files changed, 3052 insertions(+), 2181 deletions(-) create mode 100644 .github/scripts/validate_skill.py create mode 100644 .github/workflows/ci.yml create mode 100644 skills/embedded-debugger/SKILL.md create mode 100644 skills/embedded-debugger/agents/openai.yaml create mode 100644 skills/embedded-debugger/references/default-prompt.md diff --git a/.github/scripts/validate_skill.py b/.github/scripts/validate_skill.py new file mode 100644 index 0000000..9b104a0 --- /dev/null +++ b/.github/scripts/validate_skill.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Lightweight CI validator for bundled Codex/Claude Code skills.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +def fail(message: str) -> int: + print(f"error: {message}", file=sys.stderr) + return 1 + + +def main() -> int: + if len(sys.argv) != 2: + return fail("usage: validate_skill.py ") + + skill_dir = Path(sys.argv[1]) + skill_md = skill_dir / "SKILL.md" + openai_yaml = skill_dir / "agents" / "openai.yaml" + + if not skill_md.is_file(): + return fail(f"missing {skill_md}") + if not openai_yaml.is_file(): + return fail(f"missing {openai_yaml}") + + text = skill_md.read_text(encoding="utf-8") + match = re.match(r"^---\n(.*?)\n---\n", text, re.S) + if not match: + return fail("SKILL.md missing YAML frontmatter") + + frontmatter = match.group(1) + if not re.search(r"^name:\s*embedded-debugger\s*$", frontmatter, re.M): + return fail("SKILL.md frontmatter must set name: embedded-debugger") + if not re.search(r"^description:\s*.+", frontmatter, re.M): + return fail("SKILL.md frontmatter must include description") + placeholder_marker = "TO" + "DO" + if f"[{placeholder_marker}" in text or f"{placeholder_marker}:" in text: + return fail("SKILL.md still contains placeholder markers") + + metadata = openai_yaml.read_text(encoding="utf-8") + if "Use $embedded-debugger" not in metadata: + return fail("agents/openai.yaml default prompt must mention $embedded-debugger") + + print(f"validated {skill_dir}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6863ab9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +jobs: + rust: + name: Rust release checks + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install stable Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + - name: Format + run: cargo fmt --all -- --check + + - name: Clippy + run: cargo clippy --locked --all-targets --all-features -- -D warnings + + - name: Test + run: cargo test --locked --all-targets --all-features + + - name: Docs + run: RUSTDOCFLAGS="-D warnings" cargo doc --locked --all-features --no-deps + + - name: Package + run: cargo package --locked + + - name: Validate bundled skill + run: python3 .github/scripts/validate_skill.py skills/embedded-debugger + + stm32-demo: + name: STM32 demo check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install nightly Rust + uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + workspaces: examples/STM32_demo -> target + + - name: Check demo firmware + working-directory: examples/STM32_demo + env: + CARGO_TARGET_DIR: /tmp/embedded-debugger-mcp-stm32-target + run: cargo +nightly check --locked diff --git a/Cargo.lock b/Cargo.lock index 6ee8e6e..3f5405d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -19,19 +10,13 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -43,9 +28,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -58,44 +43,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "async-io" @@ -110,7 +95,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.0.8", + "rustix 1.1.4", "slab", "windows-sys 0.60.2", ] @@ -126,37 +111,15 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] @@ -167,24 +130,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -215,9 +163,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bitvec" @@ -242,28 +190,28 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.0" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "441473f2b4b0459a68628c744bc61d23e730fb00128b841d30fa4bb3972257e4" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] @@ -274,32 +222,32 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "cc" -version = "1.2.31" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -310,9 +258,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.42" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -320,9 +268,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.42" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -332,27 +280,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -415,9 +363,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -425,21 +373,21 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "csv-core" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] @@ -456,12 +404,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.21.0" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a79c4acb1fd5fa3d9304be4c76e031c54d2e92d172a393e24b19a14fe8532fe9" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core 0.21.0", - "darling_macro 0.21.0", + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -480,16 +428,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.0" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74875de90daf30eb59609910b84d4d368103aaec4c924824c6799b28f77d6a1d" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] @@ -505,13 +453,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.21.0" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79f8e61677d5df9167cd85265f8e5f64b215cdea3fb55eebc3e622e44c7a146" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core 0.21.0", + "darling_core 0.21.3", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] @@ -539,9 +487,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -558,13 +506,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] @@ -584,7 +532,7 @@ checksum = "4673f83edb6dfabfbc26704bd89ee95f4b164cd5db5fe8c88efda48fb0fca8d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] @@ -601,13 +549,13 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "embedded-debugger-mcp" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "async-trait", @@ -623,7 +571,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tokio-test", "toml", @@ -640,12 +588,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -683,7 +631,7 @@ dependencies = [ "serde", "sha2", "strum", - "thiserror 2.0.12", + "thiserror 2.0.18", "xmas-elf", ] @@ -716,15 +664,21 @@ checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -738,9 +692,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -753,9 +707,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -768,9 +722,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -778,15 +732,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -795,9 +749,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -811,32 +765,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -846,7 +800,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -862,14 +815,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -904,9 +856,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heapless" @@ -960,9 +912,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -984,12 +936,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -997,9 +950,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1010,11 +963,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1025,42 +977,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1076,9 +1024,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1087,9 +1035,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1103,13 +1051,14 @@ checksum = "365a784774bb381e8c19edb91190a90d7f2625e057b55de2bc0f6b57bc779ff2" [[package]] name = "indexmap" -version = "2.10.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown", "serde", + "serde_core", ] [[package]] @@ -1133,22 +1082,11 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "libc", -] - [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -1161,9 +1099,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jep106" @@ -1176,10 +1114,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1192,9 +1132,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libudev-sys" @@ -1214,31 +1154,30 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "mach2" @@ -1251,11 +1190,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -1276,9 +1215,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "miette" @@ -1299,7 +1238,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] @@ -1309,17 +1248,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.4" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -1339,26 +1279,25 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.13.0", "cfg-if", "libc", ] [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.61.2", ] [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -1399,21 +1338,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "parking" @@ -1423,9 +1356,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1433,15 +1366,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1461,27 +1394,21 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -1499,15 +1426,15 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.5.2", "pin-project-lite", - "rustix 1.0.8", + "rustix 1.1.4", "windows-sys 0.60.2", ] [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -1583,27 +1510,27 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "radium" @@ -1613,76 +1540,61 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.13.0", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rmcp" @@ -1699,7 +1611,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -1711,48 +1623,39 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aebc912b8fa7d54999adc4e45601d1d95fe458f97eb0a1277eddcd6382cf4b1" dependencies = [ - "darling 0.21.0", + "darling 0.21.3", "proc-macro2", "quote", "serde_json", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] name = "rmp" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" dependencies = [ - "byteorder", "num-traits", - "paste", ] [[package]] name = "rmp-serde" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" dependencies = [ - "byteorder", "rmp", "serde", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -1761,34 +1664,34 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.13.0", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[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 = "schemars" -version = "1.0.4" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "chrono", "dyn-clone", @@ -1800,14 +1703,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.0.4" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] @@ -1833,27 +1736,37 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] @@ -1864,19 +1777,20 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -1899,16 +1813,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64", "chrono", "hex", "indexmap", - "serde", - "serde_derive", + "serde_core", "serde_json", "time", ] @@ -1928,11 +1841,11 @@ dependencies = [ [[package]] name = "serialport" -version = "4.7.2" +version = "4.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb0bc984f6af6ef8bab54e6cf2071579ee75b9286aa9f2319a0d220c28b0a2b" +checksum = "a4d91116f97173694f1642263b2ff837f80d933aa837e2314969f6728f661df3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.13.0", "cfg-if", "core-foundation 0.10.1", "core-foundation-sys", @@ -1941,7 +1854,7 @@ dependencies = [ "nix 0.26.4", "scopeguard", "unescaper", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1966,46 +1879,53 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "slab" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strsim" @@ -2038,7 +1958,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] @@ -2060,9 +1980,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -2077,7 +1997,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] @@ -2088,15 +2008,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix 1.0.8", - "windows-sys 0.59.0", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] @@ -2110,11 +2030,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.18", ] [[package]] @@ -2125,18 +2045,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] @@ -2150,28 +2070,28 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -2179,40 +2099,37 @@ dependencies = [ [[package]] name = "tokio" -version = "1.47.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -2221,12 +2138,10 @@ dependencies = [ [[package]] name = "tokio-test" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" dependencies = [ - "async-stream", - "bytes", "futures-core", "tokio", "tokio-stream", @@ -2234,9 +2149,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2288,7 +2203,7 @@ dependencies = [ "serde_spanned", "toml_datetime", "toml_write", - "winnow 0.7.12", + "winnow 0.7.15", ] [[package]] @@ -2299,9 +2214,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2310,20 +2225,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -2342,14 +2257,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -2366,9 +2281,9 @@ checksum = "41713888c5ccfd99979fcd1afd47b71652e331b3d4a0e19d30769e80fec76cce" [[package]] name = "typenum" -version = "1.18.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "udev" @@ -2394,14 +2309,14 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c01d12e3a56a4432a8b436f293c25f4808bdf9e9f9f98f9260bba1f1bc5a1f26" dependencies = [ - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" @@ -2417,14 +2332,15 @@ checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -2441,13 +2357,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "getrandom", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -2469,46 +2385,24 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" -dependencies = [ - "wit-bindgen-rt", -] - [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.104", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2516,53 +2410,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.104", - "wasm-bindgen-backend", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", @@ -2573,46 +2445,46 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] @@ -2626,6 +2498,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -2641,7 +2522,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -2677,19 +2567,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "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]] @@ -2706,9 +2596,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -2724,9 +2614,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -2742,9 +2632,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -2754,9 +2644,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -2772,9 +2662,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -2790,9 +2680,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -2808,9 +2698,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -2826,9 +2716,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -2841,27 +2731,18 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] - [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -2883,11 +2764,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -2895,13 +2775,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", "synstructure", ] @@ -2913,50 +2793,50 @@ checksum = "2fe21bcc34ca7fe6dd56cc2cb1261ea59d6b93620215aefb5ea6032265527784" [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", "synstructure", ] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -2965,9 +2845,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -2976,11 +2856,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.118", ] + +[[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 ebb8664..e55cd25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "embedded-debugger-mcp" -version = "0.1.0" +version = "0.2.0" edition = "2021" authors = ["adancurusul "] description = "A Model Context Protocol server for embedded debugging with probe-rs - supports ARM Cortex-M, RISC-V debugging via J-Link, ST-Link, and more" @@ -64,4 +64,4 @@ path = "src/main.rs" [lib] name = "embedded_debugger_mcp" -path = "src/lib.rs" \ No newline at end of file +path = "src/lib.rs" diff --git a/README.md b/README.md index 7a67530..07d5fee 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,75 @@ # Embedded Debugger MCP Server -[![Rust](https://img.shields.io/badge/rust-1.70+-orange.svg)](https://rust-lang.org) +[![Rust](https://img.shields.io/badge/rust-stable-orange.svg)](https://rust-lang.org) [![RMCP](https://img.shields.io/badge/RMCP-0.3.2-blue.svg)](https://github.com/modelcontextprotocol/rust-sdk) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) -A professional Model Context Protocol (MCP) server for embedded debugging with probe-rs. Provides AI assistants with comprehensive debugging capabilities for embedded systems including ARM Cortex-M, RISC-V microcontrollers with real hardware integration. +Embedded Debugger MCP is a Rust server for embedded debugging through +probe-rs. It exposes MCP tools for AI assistants and also includes a small CLI +and bundled skill for users who want a command-driven workflow without setting +up an MCP client first. -> 📖 **Language Versions**: [English](README.md) | [中文](README_zh.md) +Language versions: [English](README.md) | [中文](README_zh.md) -## ✨ Features +## What It Provides -- 🚀 **Production Ready**: Real hardware integration with 22 comprehensive debugging tools -- 🔌 **Multi-Probe Support**: J-Link, ST-Link V2/V3, DAPLink, Black Magic Probe -- 🎯 **Complete Debug Control**: Connect, halt, run, reset, single-step execution -- 💾 **Memory Operations**: Read/write flash and RAM with multiple data formats -- 🛑 **Breakpoint Management**: Hardware and software breakpoints with real-time control -- 📱 **Flash Programming**: Complete flash operations - erase, program, verify -- 📡 **RTT Bidirectional**: Real-Time Transfer with interactive command/response system -- 🏗️ **Multi-Architecture**: ARM Cortex-M, RISC-V with tested STM32 integration -- 🤖 **AI Integration**: Perfect compatibility with Claude and other AI assistants -- 🧪 **Comprehensive Testing**: All 22 tools validated with real STM32G431CBTx hardware +- MCP tools for probe discovery, target connection, core control, memory access, + breakpoints, flash programming, and RTT communication. +- CLI commands for environment checks, configuration inspection, probe listing, + MCP serving, and skill prompt handoff. +- A Codex/Claude Code compatible skill at `skills/embedded-debugger`. +- Release checks covering rustfmt, clippy, tests, docs, packaging, and the STM32 + demo build. -## 🏗️ Architecture +## Architecture +```text +MCP client or CLI + | + v +embedded-debugger-mcp + | + v +probe-rs -> debug probe -> target MCU ``` -┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ MCP Client │◄──►│ Embedded │◄──►│ Debug Probe │ -│ (Claude/AI) │ │ Debugger MCP │ │ Hardware │ -└─────────────────┘ └──────────────────┘ └─────────────────┘ - │ - ▼ - ┌──────────────────┐ - │ Target Device │ - │ (ARM/RISC-V) │ - └──────────────────┘ -``` - -## 🚀 Quick Start -### Prerequisites +## Requirements -**Hardware Requirements:** -- **Debug Probe**: ST-Link V2/V3, J-Link, or DAPLink compatible probe -- **Target Board**: STM32 or other supported microcontroller -- **Connection**: USB cables for probe and target board +- Rust stable toolchain. +- A probe-rs compatible debug probe such as ST-Link, J-Link, DAPLink, Black + Magic Probe, or a supported FTDI-based probe. +- A supported target chip and working SWD/JTAG wiring for hardware operations. +- Nightly Rust plus `rust-src` for the bundled STM32 demo firmware check. -**Software Requirements:** -- Rust 1.70+ -- probe-rs compatible debug probe drivers - -### Installation +## Build ```bash -# Clone and build from source git clone https://github.com/adancurusul/embedded-debugger-mcp.git cd embedded-debugger-mcp cargo build --release ``` -### Basic Usage - -**Configure MCP Clients** +The binary is `target/release/embedded-debugger-mcp`. -#### Claude Desktop Configuration Example +## MCP Mode -Add to Claude Desktop configuration file: +Run the server explicitly: -**Windows Example:** -```json -{ - "mcpServers": { - "embedded-debugger": { - "command": "C:\\path\\to\\debugger-mcp-rs\\target\\release\\embedded-debugger-mcp.exe", - "args": [], - "env": { - "RUST_LOG": "info" - } - } - } -} +```bash +embedded-debugger-mcp serve ``` -**macOS/Linux Example:** +For compatibility, running `embedded-debugger-mcp` without a subcommand also +serves MCP over stdio. + +Example MCP client configuration: + ```json { "mcpServers": { "embedded-debugger": { - "command": "/path/to/debugger-mcp-rs/target/release/embedded-debugger-mcp", - "args": [], + "command": "/path/to/embedded-debugger-mcp/target/release/embedded-debugger-mcp", + "args": ["serve"], "env": { "RUST_LOG": "info" } @@ -96,152 +78,136 @@ Add to Claude Desktop configuration file: } ``` -Other examples for other tools like cursor ,claude code etc. please refer to the corresponding tool documentation +On Windows, use the `.exe` path and Windows path separators. -## 🎯 Try the STM32 Demo +## CLI And Skill Mode -We provide a comprehensive **STM32 RTT Bidirectional Demo** that showcases all capabilities: +CLI mode is useful for setup checks, automation, and agent workflows before an +MCP client is configured. ```bash -# Navigate to the example -cd examples/STM32_demo +embedded-debugger-mcp doctor +embedded-debugger-mcp doctor --json +embedded-debugger-mcp probes list +embedded-debugger-mcp probes list --json +embedded-debugger-mcp config generate +embedded-debugger-mcp config validate +embedded-debugger-mcp config show +embedded-debugger-mcp skill print-prompt +``` + +The bundled skill lives in: + +```text +skills/embedded-debugger/ +``` + +It is written as a plain Codex/Claude Code skill. The skill starts with CLI +checks and uses MCP tools only when an MCP client is available. + +## MCP Tool Set + +Probe management: + +| Tool | Purpose | +|------|---------| +| `list_probes` | Discover connected debug probes. | +| `connect` | Open a probe session for a target chip. | +| `probe_info` | Show active session information. | + +Target control: + +| Tool | Purpose | +|------|---------| +| `halt` | Halt core execution. | +| `run` | Resume execution. | +| `reset` | Reset the core. Only the implemented hardware-style reset path is accepted. | +| `step` | Single-step one instruction. | +| `get_status` | Read core/session status. | +| `disconnect` | Drop the session and clean up resources. | + +Memory and breakpoints: + +| Tool | Purpose | +|------|---------| +| `read_memory` | Read target memory with configured size/range limits. | +| `write_memory` | Write target memory when enabled by configuration. | +| `set_breakpoint` | Set a hardware breakpoint. | +| `clear_breakpoint` | Clear a hardware breakpoint. | + +Flash: + +| Tool | Purpose | +|------|---------| +| `flash_erase` | Erase flash when erase permissions are enabled. | +| `flash_program` | Program ELF, HEX, or BIN files through probe-rs flash algorithms. | +| `flash_verify` | Compare raw expected data with target flash contents. | +| `run_firmware` | Erase, program, reset/run, and optionally attach RTT. | + +RTT: + +| Tool | Purpose | +|------|---------| +| `rtt_attach` | Attach to a SEGGER RTT control block. | +| `rtt_detach` | Detach RTT. | +| `rtt_channels` | List discovered RTT channels. | +| `rtt_read` | Read from an up channel with max byte and timeout limits. | +| `rtt_write` | Write to a down channel. | + +## Safety Notes + +- Flash erase is disabled unless `security.allow_flash_erase` or + `flash.allow_erase` is enabled. +- Memory writes are controlled by `security.allow_memory_write`. +- Optional memory range restriction uses target memory regions in the + configuration. +- Firmware file paths are canonicalized, checked against + `security.allowed_file_paths` when configured, and checked against size + limits. +- Sector erase by arbitrary address is rejected until target-specific sector + mapping is implemented. +- `flash_verify` supports raw data comparison. Use raw BIN files or hex data for + file-based verification. + +Generate a starting configuration: -# Build the firmware -cargo build --release - -# Use with MCP server for complete debugging experience +```bash +embedded-debugger-mcp config generate > embedded-debugger.toml +embedded-debugger-mcp --config embedded-debugger.toml config validate ``` -**What the demo shows:** -- ✅ **Interactive RTT Communication**: Send commands and get real-time responses -- ✅ **All 22 MCP Tools**: Complete validation with real STM32 hardware -- ✅ **Fibonacci Calculator**: Live data streaming with control commands -- ✅ **Hardware Integration**: Tested with STM32G431CBTx + ST-Link V2 +## STM32 Demo -[📖 View STM32 Demo Documentation →](examples/STM32_demo/README.md) +The STM32 RTT demo is in `examples/STM32_demo`. -### Usage Examples with AI Assistants - -#### List Available Debug Probes -``` -Please list available debug probes on the system +```bash +cd examples/STM32_demo +CARGO_TARGET_DIR=/tmp/embedded-debugger-mcp-stm32-target cargo +nightly check --locked ``` -#### Connect and Flash Firmware -``` -Connect to my STM32G431CBTx using ST-Link probe, then flash the firmware at examples/STM32_demo/target/thumbv7em-none-eabi/release/STM32_demo -``` +The demo firmware shows multi-channel RTT communication and is intended as a +hardware validation aid. See [examples/STM32_demo/README.md](examples/STM32_demo/README.md). -#### Interactive RTT Communication -``` -Please attach RTT and show me the data from the terminal channel. Then send a command 'L' to toggle the LED. -``` +## Release Checks -#### Memory Analysis -``` -Read 64 bytes of memory from address 0x08000000 and analyze the data format -``` +Run these before cutting a release: -#### Test All 22 MCP Tools -``` -Please help me test all 22 MCP embedded debugger tools with my STM32 board. Start by connecting to the probe, then systematically test each tool category: probe management, memory operations, debug control, breakpoints, flash operations, RTT communication, and session management. +```bash +cargo fmt --all -- --check +cargo clippy --locked --all-targets --all-features -- -D warnings +cargo test --locked --all-targets --all-features +RUSTDOCFLAGS="-D warnings" cargo doc --locked --all-features --no-deps +cargo package --locked +python3 /Users/adan/.codex/skills/.system/skill-creator/scripts/quick_validate.py skills/embedded-debugger +(cd examples/STM32_demo && CARGO_TARGET_DIR=/tmp/embedded-debugger-mcp-stm32-target cargo +nightly check --locked) ``` -## 🛠️ Complete Tool Set (22 Tools) - -All tools tested and validated with real STM32 hardware: - -### 🔌 Probe Management (3 tools) -| Tool | Description | Status | -|------|-------------|---------| -| `list_probes` | Discover available debug probes | ✅ Production Ready | -| `connect` | Connect to probe and target chip | ✅ Production Ready | -| `probe_info` | Get detailed session information | ✅ Production Ready | - -### 💾 Memory Operations (2 tools) -| Tool | Description | Status | -|------|-------------|---------| -| `read_memory` | Read flash/RAM with multiple formats | ✅ Production Ready | -| `write_memory` | Write to target memory | ✅ Production Ready | - -### 🎯 Debug Control (4 tools) -| Tool | Description | Status | -|------|-------------|---------| -| `halt` | Stop target execution | ✅ Production Ready | -| `run` | Resume target execution | ✅ Production Ready | -| `reset` | Hardware/software reset | ✅ Production Ready | -| `step` | Single instruction stepping | ✅ Production Ready | - -### 🛑 Breakpoint Management (2 tools) -| Tool | Description | Status | -|------|-------------|---------| -| `set_breakpoint` | Set hardware/software breakpoints | ✅ Production Ready | -| `clear_breakpoint` | Remove breakpoints | ✅ Production Ready | - -### 📱 Flash Operations (3 tools) -| Tool | Description | Status | -|------|-------------|---------| -| `flash_erase` | Erase flash memory sectors/chip | ✅ Production Ready | -| `flash_program` | Program ELF/HEX/BIN files | ✅ Production Ready | -| `flash_verify` | Verify flash contents | ✅ Production Ready | - -### 📡 RTT Communication (6 tools) -| Tool | Description | Status | -|------|-------------|---------| -| `rtt_attach` | Connect to RTT communication | ✅ Production Ready | -| `rtt_detach` | Disconnect RTT | ✅ Production Ready | -| `rtt_channels` | List available RTT channels | ✅ Production Ready | -| `rtt_read` | Read from RTT up channels | ✅ Production Ready | -| `rtt_write` | Write to RTT down channels | ✅ Production Ready | -| `run_firmware` | Complete deployment + RTT | ✅ Production Ready | - -### 📊 Session Management (2 tools) -| Tool | Description | Status | -|------|-------------|---------| -| `get_status` | Get current debug status | ✅ Production Ready | -| `disconnect` | Clean session termination | ✅ Production Ready | - -**✅ 22/22 Tools - 100% Success Rate with Real Hardware** - -## 🌍 Supported Hardware - -### Debug Probes -- **J-Link**: Segger J-Link (all variants) -- **ST-Link**: ST-Link/V2, ST-Link/V3 -- **DAPLink**: ARM DAPLink compatible probes -- **Black Magic Probe**: Black Magic Probe -- **FTDI**: FTDI-based debug probes - -### Target Architectures -- **ARM Cortex-M**: M0, M0+, M3, M4, M7, M23, M33 -- **RISC-V**: Various RISC-V cores -- **ARM Cortex-A**: Basic support - -## 🏆 Production Status - -### ✅ Fully Implemented and Tested - -**Current Status: PRODUCTION READY** - -- ✅ **Complete probe-rs Integration**: Real hardware debugging with all 22 tools -- ✅ **Hardware Validation**: Tested with STM32G431CBTx + ST-Link V2 -- ✅ **RTT Bidirectional**: Full interactive communication with real-time commands -- ✅ **Flash Operations**: Complete erase, program, verify workflow -- ✅ **Session Management**: Multi-session support with robust error handling -- ✅ **AI Integration**: Perfect MCP protocol compatibility - -## 🙏 Acknowledgments - -Thanks to the following open source projects: - -- [probe-rs](https://probe.rs/) - Embedded debugging toolkit -- [rmcp](https://github.com/modelcontextprotocol/rust-sdk) - Rust MCP SDK -- [tokio](https://tokio.rs/) - Async runtime - -## 📄 License +## Acknowledgments -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. +- [probe-rs](https://probe.rs/) for embedded debug probe support. +- [rmcp](https://github.com/modelcontextprotocol/rust-sdk) for the Rust MCP SDK. +- [tokio](https://tokio.rs/) for the async runtime. ---- +## License -⭐ If this project helps you, please give us a Star! \ No newline at end of file +This project is licensed under the MIT License. See [LICENSE](LICENSE). diff --git a/README_zh.md b/README_zh.md index f4247d3..aef4021 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,93 +1,72 @@ # 嵌入式调试器 MCP 服务器 -[![Rust](https://img.shields.io/badge/rust-1.70+-orange.svg)](https://rust-lang.org) +[![Rust](https://img.shields.io/badge/rust-stable-orange.svg)](https://rust-lang.org) [![RMCP](https://img.shields.io/badge/RMCP-0.3.2-blue.svg)](https://github.com/modelcontextprotocol/rust-sdk) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) -专业的模型上下文协议 (MCP) 嵌入式调试服务器,基于 probe-rs 构建。为 AI 助手提供包括 ARM Cortex-M、RISC-V 微控制器在内的全面嵌入式系统调试功能,支持真实硬件集成。 +Embedded Debugger MCP 是一个基于 probe-rs 的 Rust 嵌入式调试服务器。它为 +AI 助手提供 MCP 工具,同时也提供小型 CLI 和内置 skill,让用户即使不安装 +MCP 客户端,也可以先用命令行工作流完成检查和引导。 -> 📖 **语言版本**: [English](README.md) | [中文](README_zh.md) +语言版本: [English](README.md) | [中文](README_zh.md) -## ✨ 功能特性 +## 功能 -- 🚀 **生产就绪**: 真实硬件集成,提供22个综合调试工具 -- 🔌 **多探针支持**: J-Link, ST-Link V2/V3, DAPLink, Black Magic Probe -- 🎯 **完整调试控制**: 连接、暂停、运行、复位、单步执行 -- 💾 **内存操作**: 支持多种数据格式的Flash和RAM读写 -- 🛑 **断点管理**: 硬件和软件断点的实时控制 -- 📱 **Flash编程**: 完整的Flash操作 - 擦除、编程、验证 -- 📡 **RTT双向通信**: 实时传输,支持交互式命令/响应系统 -- 🏗️ **多架构支持**: ARM Cortex-M, RISC-V,经过STM32集成测试 -- 🤖 **AI集成**: 与Claude和其他AI助手完美兼容 -- 🧪 **全面测试**: 所有22个工具在真实STM32G431CBTx硬件上验证通过 +- MCP 工具覆盖探针发现、目标连接、核心控制、内存访问、断点、Flash 编程和 + RTT 通信。 +- CLI 命令覆盖环境检查、配置查看、探针列表、MCP 启动和 skill prompt 输出。 +- 内置 Codex / Claude Code 兼容 skill: `skills/embedded-debugger`。 +- 发布检查覆盖 rustfmt、clippy、测试、文档、打包和 STM32 demo 构建。 -## 🏗️ 架构 +## 架构 +```text +MCP client or CLI + | + v +embedded-debugger-mcp + | + v +probe-rs -> debug probe -> target MCU ``` -┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ MCP 客户端 │◄──►│ 嵌入式调试器 │◄──►│ 调试探针 │ -│ (Claude/AI) │ │ MCP 服务器 │ │ 硬件 │ -└─────────────────┘ └──────────────────┘ └─────────────────┘ - │ - ▼ - ┌──────────────────┐ - │ 目标设备 │ - │ (ARM/RISC-V) │ - └──────────────────┘ -``` - -## 🚀 快速开始 - -### 前置要求 -**硬件要求:** -- **调试探针**: ST-Link V2/V3, J-Link, 或 DAPLink 兼容探针 -- **目标板**: STM32 或其他支持的微控制器 -- **连接线**: 用于探针和目标板的USB线 +## 要求 -**软件要求:** -- Rust 1.70+ -- probe-rs 兼容的调试探针驱动程序 +- Rust stable 工具链。 +- probe-rs 兼容调试探针,例如 ST-Link、J-Link、DAPLink、Black Magic Probe + 或受支持的 FTDI 探针。 +- 目标芯片和可工作的 SWD/JTAG 连线。 +- STM32 demo 固件检查需要 nightly Rust 和 `rust-src`。 -### 安装 +## 构建 ```bash -# 克隆并从源码构建 git clone https://github.com/adancurusul/embedded-debugger-mcp.git cd embedded-debugger-mcp cargo build --release ``` -### 基本使用 +二进制位于 `target/release/embedded-debugger-mcp`。 -**配置 MCP 客户端** +## MCP 模式 -#### Claude Desktop 配置示例 +显式启动服务器: -添加到 Claude Desktop 配置文件: - -**Windows 示例:** -```json -{ - "mcpServers": { - "embedded-debugger": { - "command": "C:\\path\\to\\debugger-mcp-rs\\target\\release\\embedded-debugger-mcp.exe", - "args": [], - "env": { - "RUST_LOG": "info" - } - } - } -} +```bash +embedded-debugger-mcp serve ``` -**macOS/Linux 示例:** +为了兼容旧配置,不带子命令运行 `embedded-debugger-mcp` 也会通过 stdio +启动 MCP 服务。 + +MCP 客户端配置示例: + ```json { "mcpServers": { "embedded-debugger": { - "command": "/path/to/debugger-mcp-rs/target/release/embedded-debugger-mcp", - "args": [], + "command": "/path/to/embedded-debugger-mcp/target/release/embedded-debugger-mcp", + "args": ["serve"], "env": { "RUST_LOG": "info" } @@ -96,156 +75,131 @@ cargo build --release } ``` -其他例如cursor ,claude code 等参考对应工具文档 +Windows 下请使用 `.exe` 路径和 Windows 路径分隔符。 -## 🎯 试试 STM32 演示 +## CLI 与 Skill 模式 -我们提供了一个全面的 **STM32 RTT 双向通信演示**,展示了所有功能: +CLI 模式适合在 MCP 客户端配置前做环境检查、自动化和 agent 工作流。 ```bash -# 进入示例目录 -cd examples/STM32_demo - -# 构建固件 -cargo build --release - -# 与 MCP 服务器配合使用,获得完整的调试体验 -``` - -**演示内容:** -- ✅ **交互式 RTT 通信**: 发送命令并获得实时响应 -- ✅ **全部 22 个 MCP 工具**: 在真实 STM32 硬件上完整验证 -- ✅ **斐波那契计算器**: 实时数据流与控制命令 -- ✅ **硬件集成**: 在 STM32G431CBTx + ST-Link V2 上测试 - -[📖 查看 STM32 演示文档 →](examples/STM32_demo/README.md) - -### AI 助手使用示例 - -#### 列出可用的调试探针 -``` -请列出系统上可用的调试探针 -``` - -#### 连接并烧录固件 -``` -使用 ST-Link 探针连接到我的 STM32G431CBTx,然后烧录位于 examples/STM32_demo/target/thumbv7em-none-eabi/release/STM32_demo 的固件 -``` - -#### 交互式 RTT 通信 -``` -请连接 RTT 并显示终端通道的数据。然后发送命令 'L' 来切换 LED。 +embedded-debugger-mcp doctor +embedded-debugger-mcp doctor --json +embedded-debugger-mcp probes list +embedded-debugger-mcp probes list --json +embedded-debugger-mcp config generate +embedded-debugger-mcp config validate +embedded-debugger-mcp config show +embedded-debugger-mcp skill print-prompt ``` -#### 内存分析 -``` -读取地址 0x08000000 处的 64 字节内存并分析数据格式 -``` +内置 skill 位于: -#### 测试全部 22 个 MCP 工具 -``` -请帮我测试所有 22 个 MCP 嵌入式调试工具与我的 STM32 开发板。先连接到探针,然后系统性地测试每个工具类别:探针管理、内存操作、调试控制、断点、Flash 操作、RTT 通信和会话管理。 +```text +skills/embedded-debugger/ ``` -## 🛠️ 完整工具集 (22个工具) +它是普通 Codex / Claude Code skill。工作流会先运行 CLI 检查;只有 MCP 客户端 +可用时,才会使用 MCP 工具做会话型调试操作。 -所有工具均通过真实 STM32 硬件测试和验证: +## MCP 工具集 -### 🔌 探针管理 (3个工具) -| 工具 | 描述 | 状态 | -|------|------|------| -| `list_probes` | 发现可用的调试探针 | ✅ 生产就绪 | -| `connect` | 连接到探针和目标芯片 | ✅ 生产就绪 | -| `probe_info` | 获取详细会话信息 | ✅ 生产就绪 | +探针管理: -### 💾 内存操作 (2个工具) -| 工具 | 描述 | 状态 | -|------|------|------| -| `read_memory` | 支持多种格式的Flash/RAM读取 | ✅ 生产就绪 | -| `write_memory` | 向目标内存写入数据 | ✅ 生产就绪 | +| 工具 | 用途 | +|------|------| +| `list_probes` | 发现连接的调试探针。 | +| `connect` | 为目标芯片打开探针会话。 | +| `probe_info` | 查看活动会话信息。 | -### 🎯 调试控制 (4个工具) -| 工具 | 描述 | 状态 | -|------|------|------| -| `halt` | 停止目标执行 | ✅ 生产就绪 | -| `run` | 恢复目标执行 | ✅ 生产就绪 | -| `reset` | 硬件/软件复位 | ✅ 生产就绪 | -| `step` | 单指令步进 | ✅ 生产就绪 | +目标控制: -### 🛑 断点管理 (2个工具) -| 工具 | 描述 | 状态 | -|------|------|------| -| `set_breakpoint` | 设置硬件/软件断点 | ✅ 生产就绪 | -| `clear_breakpoint` | 移除断点 | ✅ 生产就绪 | +| 工具 | 用途 | +|------|------| +| `halt` | 暂停核心执行。 | +| `run` | 恢复执行。 | +| `reset` | 复位核心。当前服务器只接受已实现的硬件风格复位路径。 | +| `step` | 单步执行一条指令。 | +| `get_status` | 读取核心和会话状态。 | +| `disconnect` | 断开会话并清理资源。 | -### 📱 Flash 操作 (3个工具) -| 工具 | 描述 | 状态 | -|------|------|------| -| `flash_erase` | 擦除Flash内存扇区/芯片 | ✅ 生产就绪 | -| `flash_program` | 编程 ELF/HEX/BIN 文件 | ✅ 生产就绪 | -| `flash_verify` | 验证Flash内容 | ✅ 生产就绪 | +内存与断点: -### 📡 RTT 通信 (6个工具) -| 工具 | 描述 | 状态 | -|------|------|------| -| `rtt_attach` | 连接到RTT通信 | ✅ 生产就绪 | -| `rtt_detach` | 断开RTT连接 | ✅ 生产就绪 | -| `rtt_channels` | 列出可用的RTT通道 | ✅ 生产就绪 | -| `rtt_read` | 从RTT上行通道读取 | ✅ 生产就绪 | -| `rtt_write` | 向RTT下行通道写入 | ✅ 生产就绪 | -| `run_firmware` | 完整部署 + RTT | ✅ 生产就绪 | +| 工具 | 用途 | +|------|------| +| `read_memory` | 在配置的大小和范围限制内读取目标内存。 | +| `write_memory` | 在配置允许时写入目标内存。 | +| `set_breakpoint` | 设置硬件断点。 | +| `clear_breakpoint` | 清除硬件断点。 | -### 📊 会话管理 (2个工具) -| 工具 | 描述 | 状态 | -|------|------|------| -| `get_status` | 获取当前调试状态 | ✅ 生产就绪 | -| `disconnect` | 清理会话终止 | ✅ 生产就绪 | +Flash: -**✅ 22/22 工具 - 真实硬件 100% 成功率** +| 工具 | 用途 | +|------|------| +| `flash_erase` | 在擦除权限开启时擦除 Flash。 | +| `flash_program` | 通过 probe-rs Flash 算法烧录 ELF、HEX 或 BIN。 | +| `flash_verify` | 将原始期望数据与目标 Flash 内容比较。 | +| `run_firmware` | 擦除、烧录、复位/运行,并可选择连接 RTT。 | -## 🌍 支持的硬件 +RTT: -### 调试探针 -- **J-Link**: Segger J-Link (所有变体) -- **ST-Link**: ST-Link/V2, ST-Link/V3 -- **DAPLink**: ARM DAPLink 兼容探针 -- **Black Magic Probe**: Black Magic Probe -- **FTDI**: 基于 FTDI 的调试探针 +| 工具 | 用途 | +|------|------| +| `rtt_attach` | 连接 SEGGER RTT 控制块。 | +| `rtt_detach` | 断开 RTT。 | +| `rtt_channels` | 列出发现的 RTT 通道。 | +| `rtt_read` | 从上行通道读取,并遵守最大字节数和超时限制。 | +| `rtt_write` | 向下行通道写入。 | -### 目标架构 -- **ARM Cortex-M**: M0, M0+, M3, M4, M7, M23, M33 -- **RISC-V**: 各种 RISC-V 核心 -- **ARM Cortex-A**: 基本支持 +## 安全说明 +- 除非启用 `security.allow_flash_erase` 或 `flash.allow_erase`,否则 Flash + 擦除会被拒绝。 +- 内存写入受 `security.allow_memory_write` 控制。 +- 可选内存范围限制使用配置中的目标 memory region。 +- 固件文件路径会做 canonicalize;如果配置了 `security.allowed_file_paths`, + 还会检查路径是否在允许目录内,并检查文件大小限制。 +- 任意地址的 sector erase 目前会被拒绝,直到实现目标相关的 sector 映射。 +- `flash_verify` 做原始数据比较。文件方式验证请使用原始 BIN 文件或十六进制数据。 -## 🏆 生产状态 +生成起始配置: -### ✅ 完全实现并测试 - -**当前状态: 生产就绪** +```bash +embedded-debugger-mcp config generate > embedded-debugger.toml +embedded-debugger-mcp --config embedded-debugger.toml config validate +``` -- ✅ **完整的 probe-rs 集成**: 所有22个工具的真实硬件调试 -- ✅ **硬件验证**: 在 STM32G431CBTx + ST-Link V2 上测试 -- ✅ **RTT 双向通信**: 完整的交互式通信与实时命令 -- ✅ **Flash 操作**: 完整的擦除、编程、验证工作流 -- ✅ **会话管理**: 多会话支持与强大的错误处理 -- ✅ **AI 集成**: 完美的 MCP 协议兼容性 +## STM32 Demo -## 🙏 致谢 +STM32 RTT demo 位于 `examples/STM32_demo`。 -感谢以下开源项目: +```bash +cd examples/STM32_demo +CARGO_TARGET_DIR=/tmp/embedded-debugger-mcp-stm32-target cargo +nightly check --locked +``` -- [probe-rs](https://probe.rs/) - 嵌入式调试工具包 -- [rmcp](https://github.com/modelcontextprotocol/rust-sdk) - Rust MCP SDK -- [tokio](https://tokio.rs/) - 异步运行时 +该 demo 展示多通道 RTT 通信,主要用于硬件验证。详见 +[examples/STM32_demo/README.md](examples/STM32_demo/README.md)。 +## 发布检查 +发布前运行: +```bash +cargo fmt --all -- --check +cargo clippy --locked --all-targets --all-features -- -D warnings +cargo test --locked --all-targets --all-features +RUSTDOCFLAGS="-D warnings" cargo doc --locked --all-features --no-deps +cargo package --locked +python3 /Users/adan/.codex/skills/.system/skill-creator/scripts/quick_validate.py skills/embedded-debugger +(cd examples/STM32_demo && CARGO_TARGET_DIR=/tmp/embedded-debugger-mcp-stm32-target cargo +nightly check --locked) +``` -## 📄 许可证 +## 致谢 -本项目采用 MIT 许可证。详细信息请参阅 [LICENSE](LICENSE) 文件。 ---- +- [probe-rs](https://probe.rs/) 提供嵌入式调试探针支持。 +- [rmcp](https://github.com/modelcontextprotocol/rust-sdk) 提供 Rust MCP SDK。 +- [tokio](https://tokio.rs/) 提供异步运行时。 -⭐ 如果这个项目对你有帮助,请给我们一个 Star! +## 许可证 +本项目采用 MIT 许可证。详见 [LICENSE](LICENSE)。 diff --git a/examples/README.md b/examples/README.md index a69b7c4..bd50432 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,21 +1,28 @@ -# MCP Embedded Debugger Examples +# Embedded Debugger Examples -This directory contains examples and utilities demonstrating the capabilities of the MCP embedded debugger. +This directory contains example firmware and notes for exercising +embedded-debugger-mcp with real hardware. -## Examples +## STM32_demo -### 🚀 [STM32_demo](STM32_demo/) +[STM32_demo](STM32_demo/) is an RTT bidirectional communication demo for an +STM32G431CBTx-class target. -**Complete RTT Bidirectional Communication Demo** +It demonstrates: -A comprehensive example showcasing all 22 MCP embedded debugger tools with real STM32 hardware: +- 5 RTT channels: 3 up channels and 2 down channels. +- Interactive command/response over RTT. +- Fibonacci data streaming with runtime control. +- A target firmware that can be used while testing MCP flash, debug, and RTT + workflows. -- **5-Channel RTT**: Bidirectional communication (3 up + 2 down channels) -- **Interactive Debugging**: Real-time command/response system -- **Data Streaming**: Continuous Fibonacci calculations with control -- **Complete Testing**: Validates all 22 MCP tools with 100% success rate +Hardware used during development: STM32G431CBTx with ST-Link V2. -**Hardware**: STM32G431CBTx + ST-Link V2 -**Status**: ✅ Production Ready +Build check: -[View STM32_demo Documentation →](STM32_demo/README.md) +```bash +cd STM32_demo +CARGO_TARGET_DIR=/tmp/embedded-debugger-mcp-stm32-target cargo +nightly check --locked +``` + +See [STM32_demo/README.md](STM32_demo/README.md). diff --git a/examples/STM32_demo/.cargo/config.toml b/examples/STM32_demo/.cargo/config.toml index d88b032..42ffb18 100644 --- a/examples/STM32_demo/.cargo/config.toml +++ b/examples/STM32_demo/.cargo/config.toml @@ -11,4 +11,3 @@ DEFMT_LOG = "trace" [unstable] build-std = ["core"] -build-std-features = ["panic_immediate_abort"] diff --git a/examples/STM32_demo/Cargo.lock b/examples/STM32_demo/Cargo.lock index 4d93f3b..728634a 100644 --- a/examples/STM32_demo/Cargo.lock +++ b/examples/STM32_demo/Cargo.lock @@ -2,11 +2,30 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "STM32_demo" +version = "0.1.0" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "embassy-executor", + "embassy-stm32", + "embassy-sync 0.6.2", + "embassy-time", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-io", + "embedded-io-async", + "embedded-storage", + "panic-halt", + "rtt-target", +] + [[package]] name = "aligned" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377e4c0ba83e4431b10df45c1d4666f178ea9c552cac93e60c3a88bf32785923" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" dependencies = [ "as-slice", ] @@ -22,15 +41,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "az" -version = "1.2.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bare-metal" @@ -43,9 +56,9 @@ dependencies = [ [[package]] name = "bit_field" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitfield" @@ -55,9 +68,9 @@ checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-device-driver" @@ -68,12 +81,6 @@ dependencies = [ "aligned", ] -[[package]] -name = "byte-slice-cast" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" - [[package]] name = "byteorder" version = "1.5.0" @@ -82,9 +89,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cortex-m" @@ -116,7 +123,7 @@ checksum = "e37549a379a9e0e6e576fd208ee60394ccb8be963889eebba3ffe0980364f472" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -146,7 +153,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.100", + "syn", ] [[package]] @@ -157,76 +164,45 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.100", -] - -[[package]] -name = "demo_code" -version = "0.1.0" -dependencies = [ - "cortex-m", - "cortex-m-rt", - "embassy-executor", - "embassy-stm32", - "embassy-sync", - "embassy-time", - "embedded-hal 1.0.0", - "embedded-hal-async", - "embedded-io", - "embedded-io-async", - "embedded-storage", - "panic-halt", - "rtt-target", - "ssd1306", + "syn", ] [[package]] -name = "display-interface" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba2aab1ef3793e6f7804162debb5ac5edb93b3d650fbcc5aeb72fcd0e6c03a0" - -[[package]] -name = "display-interface-i2c" -version = "0.5.0" +name = "document-features" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d964fa85bbbb5a6ecd06e58699407ac5dc3e3ad72dac0ab7e6b0d00a1cd262d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ - "display-interface", - "embedded-hal 1.0.0", - "embedded-hal-async", + "litrs", ] [[package]] -name = "display-interface-spi" -version = "0.5.0" +name = "embassy-embedded-hal" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9ec30048b1955da2038fcc3c017f419ab21bb0001879d16c0a3749dc6b7a" +checksum = "8c62a3bf127e03832fb97d8b01a058775e617653bc89e2a12c256485a7fb54c1" dependencies = [ - "byte-slice-cast", - "display-interface", + "embassy-embedded-hal 0.4.0", + "embassy-futures", + "embassy-sync 0.6.2", + "embassy-time", + "embedded-hal 0.2.7", "embedded-hal 1.0.0", "embedded-hal-async", -] - -[[package]] -name = "document-features" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" -dependencies = [ - "litrs", + "embedded-storage", + "embedded-storage-async", + "nb 1.1.0", ] [[package]] name = "embassy-embedded-hal" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fea5ef5bed4d3468dfd44f5c9fa4cda8f54c86d4fb4ae683eacf9d39e2ea12" +checksum = "d1611b7a7ab5d1fbed84c338df26d56fd9bded58006ebb029075112ed2c5e039" dependencies = [ "embassy-futures", - "embassy-sync", - "embassy-time", + "embassy-hal-internal 0.3.0", + "embassy-sync 0.7.2", "embedded-hal 0.2.7", "embedded-hal 1.0.0", "embedded-hal-async", @@ -256,14 +232,14 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] name = "embassy-futures" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f878075b9794c1e4ac788c95b728f26aa6366d32eeb10c7051389f898f7d067" +checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01" [[package]] name = "embassy-hal-internal" @@ -276,6 +252,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "embassy-hal-internal" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95285007a91b619dc9f26ea8f55452aa6c60f7115a4edc05085cd2bd3127cd7a" +dependencies = [ + "num-traits", +] + [[package]] name = "embassy-net-driver" version = "0.2.0" @@ -297,11 +282,11 @@ dependencies = [ "cortex-m-rt", "critical-section", "document-features", - "embassy-embedded-hal", + "embassy-embedded-hal 0.3.2", "embassy-futures", - "embassy-hal-internal", + "embassy-hal-internal 0.2.0", "embassy-net-driver", - "embassy-sync", + "embassy-sync 0.6.2", "embassy-time", "embassy-time-driver", "embassy-time-queue-utils", @@ -343,6 +328,20 @@ dependencies = [ "heapless", ] +[[package]] +name = "embassy-sync" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73974a3edbd0bd286759b3d483540f0ebef705919a5f56f4fc7709066f71689b" +dependencies = [ + "cfg-if", + "critical-section", + "embedded-io-async", + "futures-core", + "futures-sink", + "heapless", +] + [[package]] name = "embassy-time" version = "0.4.0" @@ -361,9 +360,9 @@ dependencies = [ [[package]] name = "embassy-time-driver" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d45f5d833b6d98bd2aab0c2de70b18bfaa10faf661a1578fd8e5dfb15eb7eba" +checksum = "6ee71af1b3a0deaa53eaf2d39252f83504c853646e472400b763060389b9fcc9" dependencies = [ "document-features", ] @@ -380,9 +379,12 @@ dependencies = [ [[package]] name = "embassy-usb-driver" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc247028eae04174b6635104a35b1ed336aabef4654f5e87a8f32327d231970" +checksum = "340c5ce591ef58c6449e43f51d2c53efe1bf0bb6a40cbf80afa0d259c7d52c76" +dependencies = [ + "embedded-io-async", +] [[package]] name = "embassy-usb-synopsys-otg" @@ -391,7 +393,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08e753b23799329780c7ac434264026d0422044d6649ed70a73441b14a6436d7" dependencies = [ "critical-section", - "embassy-sync", + "embassy-sync 0.6.2", "embassy-usb-driver", ] @@ -404,16 +406,6 @@ dependencies = [ "nb 1.1.0", ] -[[package]] -name = "embedded-graphics-core" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba9ecd261f991856250d2207f6d8376946cd9f412a2165d3b75bc87a0bc7a044" -dependencies = [ - "az", - "byteorder", -] - [[package]] name = "embedded-hal" version = "0.2.7" @@ -487,32 +479,31 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-task", "pin-project-lite", - "pin-utils", ] [[package]] @@ -542,28 +533,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "litrs" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" - -[[package]] -name = "maybe-async-cfg" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e083394889336bc66a4eaf1011ffbfa74893e910f902a9f271fa624c61e1b2" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "pulldown-cmark", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "memchr" -version = "2.7.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "nb" @@ -597,65 +569,24 @@ checksum = "de96540e0ebde571dc55c73d60ef407c653844e6f9a1e2fdbd40c07b9252d812" [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "proc-macro-error" -version = "1.0.4" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] -[[package]] -name = "pulldown-cmark" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "679341d22c78c6c649893cbd6c3278dcbe9fc4faa62fea3a9296ae2b50c14625" -dependencies = [ - "bitflags", - "memchr", - "unicase", -] - [[package]] name = "quote" -version = "1.0.40" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -706,26 +637,11 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -[[package]] -name = "ssd1306" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea6aac2d078bbc71d9b8ac3f657335311f3b6625e9a1a96ccc29f5abfa77c56" -dependencies = [ - "display-interface", - "display-interface-i2c", - "display-interface-spi", - "embedded-graphics-core", - "embedded-hal 1.0.0", - "embedded-hal-async", - "maybe-async-cfg", -] - [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -760,20 +676,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -786,17 +691,11 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69" -[[package]] -name = "unicase" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" - [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "vcell" @@ -804,12 +703,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "void" version = "1.0.2" diff --git a/examples/STM32_demo/Cargo.toml b/examples/STM32_demo/Cargo.toml index 868a759..c1105dc 100644 --- a/examples/STM32_demo/Cargo.toml +++ b/examples/STM32_demo/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["panic-immediate-abort"] + # This file was automatically generated. [package] @@ -32,8 +34,10 @@ default = [] debug = 2 lto = true opt-level = 'z' +panic = "immediate-abort" [profile.dev] debug = 2 lto = true opt-level = "z" +panic = "immediate-abort" diff --git a/examples/STM32_demo/README.md b/examples/STM32_demo/README.md index 96981d5..ae169bb 100644 --- a/examples/STM32_demo/README.md +++ b/examples/STM32_demo/README.md @@ -2,129 +2,69 @@ ![STM32G4 Development Board](img/stm32g4.jpg) -A comprehensive example demonstrating RTT (Real-Time Transfer) bidirectional communication using the MCP embedded debugger with real STM32 hardware. - -## What This Demo Shows - -This example demonstrates: -- **🔄 5-Channel RTT**: 3 up channels + 2 down channels for bidirectional communication -- **📊 Interactive Debugging**: Send commands to running firmware and get real-time responses -- **🧪 Complete MCP Testing**: Validates all 22 MCP embedded debugger tools with real hardware -- **📈 Data Streaming**: Continuous Fibonacci calculations with interactive control +This example firmware demonstrates SEGGER RTT bidirectional communication with +an STM32 target and embedded-debugger-mcp. ## Hardware Requirements -### Essential Hardware -- **STM32 Development Board**: STM32G431CBTx or similar STM32 board -- **Debug Probe**: One of the following: - - ST-Link V2/V3 (most common) - - SEGGER J-Link - - CMSIS-DAPLink compatible probe -- **USB Cables**: For connecting probe to PC and powering the board +- STM32G431CBTx or a similar STM32 board. +- Debug probe: ST-Link V2/V3, SEGGER J-Link, or CMSIS-DAPLink compatible probe. +- SWD wiring: SWDIO, SWCLK, GND, and target voltage reference. +- USB power or external target power. -### Connection -- Connect debug probe to STM32 board via SWD pins (SWDIO, SWCLK, GND, VCC) -- Connect debug probe to PC via USB -- Power the STM32 board (via USB or external power) +## Build Check -## Quick Demo +This firmware uses `build-std`, so run it with nightly Rust and `rust-src`. -### 1. Build the Firmware ```bash +rustup component add rust-src --toolchain nightly cd examples/STM32_demo -cargo build --release +CARGO_TARGET_DIR=/tmp/embedded-debugger-mcp-stm32-target cargo +nightly check --locked ``` -### 2. Use with MCP Embedded Debugger -This demo is designed to work with the MCP embedded debugger server. The MCP server provides tools to: -- Flash the firmware to your STM32 board -- Establish RTT communication -- Send interactive commands and receive responses -- Monitor real-time data streams - -### 3. What You'll See -Once running, the demo provides: -- **System messages** on terminal channel -- **Fibonacci calculations** streaming on data channel -- **Debug status** information on debug channel -- **Interactive commands** you can send to control the firmware - ## RTT Channels | Channel | Direction | Name | Purpose | |---------|-----------|------|---------| -| Up 0 | Target→Host | Terminal | System messages, responses | -| Up 1 | Target→Host | Data | Fibonacci calculation stream | -| Up 2 | Target→Host | Debug | Status information | -| Down 0 | Host→Target | Commands | Single-char commands (L,R,S,F,I,P,0-9) | -| Down 1 | Host→Target | Config | Multi-byte config (SPEED:n, LED:ON/OFF) | +| Up 0 | Target to host | Terminal | System messages and command responses | +| Up 1 | Target to host | Data | Fibonacci calculation stream | +| Up 2 | Target to host | Debug | Status information | +| Down 0 | Host to target | Commands | Single-character commands | +| Down 1 | Host to target | Config | Multi-byte configuration commands | ## Interactive Commands -### Quick Commands (Channel 0) -- `L` - Toggle LED -- `R` - Reset Fibonacci counter -- `F` - Get current Fibonacci value -- `I` - System information -- `0-9` - Set calculation speed - -### Configuration (Channel 1) -- `SPEED:3` - Set speed to 3x -- `LED:ON` - Turn LED on -- `MODE:AUTO` - Set auto mode - -## MCP Tools Testing - -This demo serves as a comprehensive test platform for all 22 MCP embedded debugger tools: - -- **Probe Management** (3 tools): Connection and probe detection -- **Memory Operations** (2 tools): Read/write memory operations -- **Debug Control** (4 tools): Halt, run, step, reset -- **Breakpoints** (2 tools): Set and clear breakpoints -- **Flash Operations** (3 tools): Erase, program, verify flash -- **RTT Communication** (6 tools): Real-time data transfer -- **Session Management** (2 tools): Status monitoring and disconnect - -**✅ All 22 tools tested successfully with 100% success rate** - -## Technical Features - -### RTT Implementation -- **5 channels total**: 3 up (target→host) + 2 down (host→target) -- **Non-blocking communication**: Real-time data flow without interrupting firmware -- **Multiple data types**: Text messages, binary data, structured information - -### Performance Characteristics -- **RTT Connection**: Reliable first-attempt attachment -- **Command Response**: <10ms latency for interactive commands -- **Flash Programming**: ~1.3 seconds for 510KB firmware -- **Session Stability**: Tested for 55+ minutes continuous operation - -## Documentation +Channel 0 commands: -Detailed technical documentation available in `docs/`: +- `L`: toggle LED +- `R`: reset Fibonacci counter +- `F`: get current Fibonacci value +- `I`: print system information +- `0` to `9`: set calculation speed -- [RTT Implementation Design](docs/RTT_BIDIRECTIONAL_DESIGN.md) - Technical implementation details -- [Performance Testing Results](docs/RTT_TESTING_RESULTS.md) - Performance benchmarks -- [Testing Summary](docs/ALL_22_TOOLS_TESTING_SUMMARY.md) - Executive summary +Channel 1 examples: -## Getting Started +- `SPEED:3`: set speed multiplier +- `LED:ON`: request LED on +- `MODE:AUTO`: request automatic mode -### Prerequisites -1. **Hardware Setup**: Connect your STM32 board and debug probe -2. **MCP Server**: Run the embedded debugger MCP server -3. **Build Firmware**: Use `cargo build --release` to compile +## Using With embedded-debugger-mcp -### Demo Features -- **Fibonacci Calculator**: Real-time mathematical calculations -- **LED Control**: Interactive hardware control via commands -- **System Monitoring**: Debug information and status reporting -- **Configuration**: Runtime parameter adjustment +Typical MCP flow: -This is a demonstration example showing the capabilities of RTT bidirectional communication with real embedded hardware. +1. `list_probes` +2. `connect` +3. `flash_program` or `run_firmware` +4. `rtt_attach` +5. `rtt_channels` +6. `rtt_read` and `rtt_write` +7. `disconnect` ---- +The demo is useful for hardware validation, RTT channel discovery, and command +round-trip checks. Results depend on the target board, probe, wiring, firmware +build, and local probe-rs support. -**Status: ✅ Hardware Validated - Real STM32G431CBTx Testing** +## Additional Notes -Perfect for demonstrating professional embedded debugging workflows with interactive real-time communication. \ No newline at end of file +The `docs/` directory contains historical design and test notes from development. +Treat those files as development evidence, not as current release guarantees. diff --git a/examples/STM32_demo/rust-toolchain.toml b/examples/STM32_demo/rust-toolchain.toml index 4ba1255..d5752b1 100644 --- a/examples/STM32_demo/rust-toolchain.toml +++ b/examples/STM32_demo/rust-toolchain.toml @@ -1,6 +1,6 @@ # This file was automatically generated. [toolchain] -channel = "1.84" +channel = "nightly" components = ["rust-src", "rustfmt"] targets = ["thumbv7em-none-eabi"] diff --git a/skills/embedded-debugger/SKILL.md b/skills/embedded-debugger/SKILL.md new file mode 100644 index 0000000..14f4e47 --- /dev/null +++ b/skills/embedded-debugger/SKILL.md @@ -0,0 +1,64 @@ +--- +name: embedded-debugger +description: Embedded hardware debugging workflow for probe-rs targets using embedded-debugger-mcp. Use when Codex or Claude Code needs to inspect debug probes, validate embedded debugger setup, start the MCP server, guide a user through ARM Cortex-M/RISC-V flashing/debugging/RTT workflows, or operate without installing an MCP client by using the CLI plus prompts. +--- + +# Embedded Debugger + +Use the local `embedded-debugger-mcp` binary as the source of truth. Prefer CLI +checks first, then MCP tools when an MCP client is available. + +## Entry Decision + +1. If the user has an MCP client configured, start or verify the server: + `embedded-debugger-mcp serve` +2. If the user wants no MCP install, use CLI-first mode: + `embedded-debugger-mcp doctor`, `embedded-debugger-mcp probes list`, and + `embedded-debugger-mcp skill print-prompt`. +3. If hardware access is required, confirm the probe and target are connected + before destructive actions such as flash erase or program. + +## CLI Workflow + +Run these in order and report the exact outcome: + +```bash +embedded-debugger-mcp doctor +embedded-debugger-mcp probes list +embedded-debugger-mcp config show +``` + +Use JSON for automation: + +```bash +embedded-debugger-mcp doctor --json +embedded-debugger-mcp probes list --json +``` + +## MCP Workflow + +Use MCP tools for session-based operations: + +1. `list_probes` +2. `connect` +3. Read-only checks such as `probe_info`, `get_status`, and `read_memory` +4. Mutating operations only after the user confirms target, file path, and risk: + `write_memory`, `flash_erase`, `flash_program`, `run_firmware` +5. RTT operations after firmware is running: `rtt_attach`, `rtt_channels`, + `rtt_read`, `rtt_write`, `rtt_detach` +6. `disconnect` + +## Safety Rules + +- Treat flash erase, flash program, memory write, reset, run, and RTT write as + mutating hardware operations. +- Prefer read-only discovery before mutation. +- Respect project configuration limits for file paths, file sizes, memory + ranges, and flash erase permissions. +- Do not claim hardware success from command text alone; cite the command or MCP + tool result that produced the evidence. + +## Prompt Reference + +For a reusable CLI+Skill prompt, read +`references/default-prompt.md`. diff --git a/skills/embedded-debugger/agents/openai.yaml b/skills/embedded-debugger/agents/openai.yaml new file mode 100644 index 0000000..2c29150 --- /dev/null +++ b/skills/embedded-debugger/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Embedded Debugger" + short_description: "Debug embedded targets with CLI or MCP" + default_prompt: "Use $embedded-debugger to inspect my embedded target setup and suggest the next debug command." diff --git a/skills/embedded-debugger/references/default-prompt.md b/skills/embedded-debugger/references/default-prompt.md new file mode 100644 index 0000000..5aa4fe7 --- /dev/null +++ b/skills/embedded-debugger/references/default-prompt.md @@ -0,0 +1,13 @@ +Use the embedded-debugger skill to inspect this embedded debugging setup. + +Work CLI-first unless an MCP client is already configured: + +1. Run `embedded-debugger-mcp doctor`. +2. Run `embedded-debugger-mcp probes list`. +3. If MCP is available, start or verify `embedded-debugger-mcp serve` and use + MCP tools for session operations. +4. Before any flash erase, flash program, memory write, reset, run, or RTT write, + state the exact target, probe, file path or address range, and why the + operation is needed. +5. Report concrete command or tool output as evidence. Do not infer hardware + success without a successful CLI command or MCP tool result. diff --git a/src/config.rs b/src/config.rs index 189cc9e..0640883 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,14 +1,14 @@ //! Configuration management for the debugger MCP server +use crate::error::{DebugError, Result}; +use clap::{Parser, Subcommand}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; -use clap::Parser; -use crate::error::{DebugError, Result}; /// Command line arguments #[derive(Parser, Debug)] -#[command(name = "debugger-mcp-rs")] +#[command(name = "embedded-debugger-mcp")] #[command(about = "A Model Context Protocol server for embedded debugging")] #[command(version)] pub struct Args { @@ -71,6 +71,67 @@ pub struct Args { /// Show current configuration and exit #[arg(long)] pub show_config: bool, + + /// CLI command to run. Defaults to serving MCP over stdio when omitted. + #[command(subcommand)] + pub command: Option, +} + +/// Top-level CLI commands. +#[derive(Subcommand, Debug, Clone, PartialEq, Eq)] +pub enum Command { + /// Serve the MCP server over stdio. + Serve, + /// Configuration utilities. + Config { + #[command(subcommand)] + action: ConfigCommand, + }, + /// Debug probe utilities. + Probes { + #[command(subcommand)] + action: ProbeCommand, + }, + /// Run environment checks for CLI, MCP, and hardware discovery. + Doctor { + /// Emit machine-readable JSON. + #[arg(long)] + json: bool, + }, + /// Skill helper commands for agent-driven workflows. + Skill { + #[command(subcommand)] + action: SkillCommand, + }, +} + +/// Configuration CLI commands. +#[derive(Subcommand, Debug, Clone, PartialEq, Eq)] +pub enum ConfigCommand { + /// Print a default configuration file. + Generate, + /// Validate the effective configuration. + Validate, + /// Show the effective configuration. + Show, +} + +/// Probe CLI commands. +#[derive(Subcommand, Debug, Clone, PartialEq, Eq)] +pub enum ProbeCommand { + /// List connected debug probes. + List { + /// Emit machine-readable JSON. + #[arg(long)] + json: bool, + }, +} + +/// Skill helper CLI commands. +#[derive(Subcommand, Debug, Clone, PartialEq, Eq)] +pub enum SkillCommand { + /// Print the default prompt for CLI+Skill workflows. + PrintPrompt, } /// Main configuration structure @@ -105,8 +166,9 @@ impl Config { /// Load configuration from file or create default pub fn load(config_path: Option<&PathBuf>) -> Result { if let Some(path) = config_path { - let content = std::fs::read_to_string(path) - .map_err(|e| DebugError::InvalidConfig(format!("Failed to read config file: {}", e)))?; + let content = std::fs::read_to_string(path).map_err(|e| { + DebugError::InvalidConfig(format!("Failed to read config file: {}", e)) + })?; let config: Config = toml::from_str(&content) .map_err(|e| DebugError::InvalidConfig(format!("Invalid TOML syntax: {}", e)))?; config.validate()?; @@ -134,13 +196,39 @@ impl Config { /// Validate configuration pub fn validate(&self) -> Result<()> { if self.server.max_sessions == 0 { - return Err(DebugError::InvalidConfig("max_sessions must be > 0".to_string())); + return Err(DebugError::InvalidConfig( + "max_sessions must be > 0".to_string(), + )); } if self.debugger.default_speed_khz == 0 { - return Err(DebugError::InvalidConfig("default_speed_khz must be > 0".to_string())); + return Err(DebugError::InvalidConfig( + "default_speed_khz must be > 0".to_string(), + )); } if self.rtt.buffer_size == 0 { - return Err(DebugError::InvalidConfig("rtt.buffer_size must be > 0".to_string())); + return Err(DebugError::InvalidConfig( + "rtt.buffer_size must be > 0".to_string(), + )); + } + if self.memory.max_read_size == 0 { + return Err(DebugError::InvalidConfig( + "memory.max_read_size must be > 0".to_string(), + )); + } + if self.memory.max_write_size == 0 { + return Err(DebugError::InvalidConfig( + "memory.max_write_size must be > 0".to_string(), + )); + } + if self.flash.max_binary_size == 0 { + return Err(DebugError::InvalidConfig( + "flash.max_binary_size must be > 0".to_string(), + )); + } + if self.security.max_file_size == 0 { + return Err(DebugError::InvalidConfig( + "security.max_file_size must be > 0".to_string(), + )); } Ok(()) } @@ -154,57 +242,71 @@ impl Config { /// Get default target configurations fn default_targets() -> HashMap { let mut targets = HashMap::new(); - - targets.insert("stm32f407".to_string(), TargetConfig { - name: "STM32F407VG".to_string(), - chip: "STM32F407VGTx".to_string(), - architecture: "Cortex-M4".to_string(), - flash_size: 1048576, // 1MB - ram_size: 196608, // 192KB - flash_algorithm: "STM32F4xx".to_string(), - memory_regions: vec![ - MemoryRegion { - name: "Flash".to_string(), - start: 0x08000000, - end: 0x080FFFFF, - access: "rx".to_string(), - }, - MemoryRegion { - name: "RAM".to_string(), - start: 0x20000000, - end: 0x2002FFFF, - access: "rwx".to_string(), - }, - ], - }); - - targets.insert("nrf52832".to_string(), TargetConfig { - name: "nRF52832".to_string(), - chip: "nrf52832_xxAA".to_string(), - architecture: "Cortex-M4F".to_string(), - flash_size: 524288, // 512KB - ram_size: 65536, // 64KB - flash_algorithm: "nRF52".to_string(), - memory_regions: vec![ - MemoryRegion { - name: "Flash".to_string(), - start: 0x00000000, - end: 0x0007FFFF, - access: "rx".to_string(), - }, - MemoryRegion { - name: "RAM".to_string(), - start: 0x20000000, - end: 0x2000FFFF, - access: "rwx".to_string(), - }, - ], - }); + + targets.insert( + "stm32f407".to_string(), + TargetConfig { + name: "STM32F407VG".to_string(), + chip: "STM32F407VGTx".to_string(), + architecture: "Cortex-M4".to_string(), + flash_size: 1048576, // 1MB + ram_size: 196608, // 192KB + flash_algorithm: "STM32F4xx".to_string(), + memory_regions: vec![ + MemoryRegion { + name: "Flash".to_string(), + start: 0x08000000, + end: 0x080FFFFF, + access: "rx".to_string(), + }, + MemoryRegion { + name: "RAM".to_string(), + start: 0x20000000, + end: 0x2002FFFF, + access: "rwx".to_string(), + }, + ], + }, + ); + + targets.insert( + "nrf52832".to_string(), + TargetConfig { + name: "nRF52832".to_string(), + chip: "nrf52832_xxAA".to_string(), + architecture: "Cortex-M4F".to_string(), + flash_size: 524288, // 512KB + ram_size: 65536, // 64KB + flash_algorithm: "nRF52".to_string(), + memory_regions: vec![ + MemoryRegion { + name: "Flash".to_string(), + start: 0x00000000, + end: 0x0007FFFF, + access: "rx".to_string(), + }, + MemoryRegion { + name: "RAM".to_string(), + start: 0x20000000, + end: 0x2000FFFF, + access: "rwx".to_string(), + }, + ], + }, + ); targets } } +impl From for Config { + fn from(max_sessions: usize) -> Self { + let mut config = Config::default(); + config.server.max_sessions = max_sessions; + config + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ServerConfig { pub max_sessions: usize, @@ -285,10 +387,10 @@ pub struct MemoryConfig { impl Default for MemoryConfig { fn default() -> Self { Self { - max_read_size: 65536, // 64KB - max_write_size: 4096, // 4KB + max_read_size: 65536, // 64KB + max_write_size: 4096, // 4KB cache_enable: true, - cache_size: 1048576, // 1MB + cache_size: 1048576, // 1MB } } } @@ -309,7 +411,7 @@ impl Default for FlashConfig { default_program_timeout_ms: 60000, verify_after_program: true, allow_erase: false, - max_binary_size: 10485760, // 10MB + max_binary_size: 10485760, // 10MB } } } @@ -330,7 +432,7 @@ impl Default for SecurityConfig { allow_memory_write: true, restrict_memory_access: false, allowed_file_paths: vec![], - max_file_size: 10485760, // 10MB + max_file_size: 10485760, // 10MB } } } @@ -351,7 +453,7 @@ pub struct MemoryRegion { pub name: String, pub start: u64, pub end: u64, - pub access: String, // "r", "w", "x", "rw", "rx", "rwx" + pub access: String, // "r", "w", "x", "rw", "rx", "rwx" } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -375,4 +477,4 @@ impl Default for LoggingConfig { include_thread_names: false, } } -} \ No newline at end of file +} diff --git a/src/debugger/discovery.rs b/src/debugger/discovery.rs index 1e78e78..bf236a3 100644 --- a/src/debugger/discovery.rs +++ b/src/debugger/discovery.rs @@ -1,12 +1,13 @@ //! Debug probe discovery and enumeration -use probe_rs::probe::list::Lister; use crate::error::{DebugError, Result}; use crate::utils::ProbeType; +use probe_rs::probe::list::Lister; +use serde::Serialize; use tracing::{debug, info, warn}; /// Information about discovered debug probe -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct ProbeInfo { pub identifier: String, pub vendor_id: u16, @@ -24,15 +25,16 @@ impl ProbeDiscovery { /// List all available debug probes pub fn list_probes() -> Result> { debug!("Discovering debug probes"); - + let lister = Lister::new(); - + let probes = lister .list_all() .into_iter() .map(|probe_info| { - let probe_type = ProbeType::from_vid_pid(probe_info.vendor_id, probe_info.product_id); - + let probe_type = + ProbeType::from_vid_pid(probe_info.vendor_id, probe_info.product_id); + ProbeInfo { identifier: probe_info.identifier.clone(), vendor_id: probe_info.vendor_id, @@ -47,10 +49,12 @@ impl ProbeDiscovery { info!("Found {} debug probes", probes.len()); for probe in &probes { - debug!(" {} - {} ({})", - probe.identifier, - probe.probe_type, - probe.serial_number.as_deref().unwrap_or("no serial")); + debug!( + " {} - {} ({})", + probe.identifier, + probe.probe_type, + probe.serial_number.as_deref().unwrap_or("no serial") + ); } Ok(probes) @@ -63,17 +67,25 @@ impl ProbeDiscovery { product_id: Option, probe_type: Option<&str>, ) -> Result { - debug!("Finding probe with criteria: serial={:?}, vid={:?}, pid={:?}, type={:?}", - serial_number, vendor_id, product_id, probe_type); - + debug!( + "Finding probe with criteria: serial={:?}, vid={:?}, pid={:?}, type={:?}", + serial_number, vendor_id, product_id, probe_type + ); + let all_probes = Self::list_probes()?; - + if all_probes.is_empty() { - return Err(DebugError::ProbeNotFound("No debug probes found".to_string())); + return Err(DebugError::ProbeNotFound( + "No debug probes found".to_string(), + )); } // If no criteria specified, return the first probe - if serial_number.is_none() && vendor_id.is_none() && product_id.is_none() && probe_type.is_none() { + if serial_number.is_none() + && vendor_id.is_none() + && product_id.is_none() + && probe_type.is_none() + { return Ok(all_probes[0].clone()); } @@ -115,15 +127,19 @@ impl ProbeDiscovery { if matching_probes.is_empty() { return Err(DebugError::ProbeNotFound( - "No probe found matching the specified criteria".to_string() + "No probe found matching the specified criteria".to_string(), )); } if matching_probes.len() > 1 { warn!("Multiple probes match criteria, using the first one"); for (i, probe) in matching_probes.iter().enumerate() { - debug!(" {}: {} ({})", i, probe.identifier, - probe.serial_number.as_deref().unwrap_or("no serial")); + debug!( + " {}: {} ({})", + i, + probe.identifier, + probe.serial_number.as_deref().unwrap_or("no serial") + ); } } @@ -133,20 +149,25 @@ impl ProbeDiscovery { /// Auto-select the best available probe pub fn auto_select_probe() -> Result { debug!("Auto-selecting debug probe"); - + let all_probes = Self::list_probes()?; - + if all_probes.is_empty() { - return Err(DebugError::ProbeNotFound("No debug probes found".to_string())); + return Err(DebugError::ProbeNotFound( + "No debug probes found".to_string(), + )); } // Prefer probes in this order: J-Link, ST-Link, DAPLink, others let preferred_order = ["j-link", "st-link", "daplink"]; - + for preferred_type in &preferred_order { for probe in &all_probes { if probe.probe_type.to_lowercase().contains(preferred_type) { - info!("Auto-selected probe: {} ({})", probe.identifier, probe.probe_type); + info!( + "Auto-selected probe: {} ({})", + probe.identifier, probe.probe_type + ); return Ok(probe.clone()); } } @@ -154,16 +175,19 @@ impl ProbeDiscovery { // If no preferred probe found, use the first one let selected = &all_probes[0]; - info!("Auto-selected probe: {} ({})", selected.identifier, selected.probe_type); + info!( + "Auto-selected probe: {} ({})", + selected.identifier, selected.probe_type + ); Ok(selected.clone()) } /// Get detailed information about a specific probe pub fn get_probe_details(identifier: &str) -> Result { debug!("Getting details for probe: {}", identifier); - + let all_probes = Self::list_probes()?; - + all_probes .into_iter() .find(|probe| probe.identifier == identifier) @@ -175,10 +199,10 @@ impl ProbeDiscovery { match probe_type { ProbeType::JLink => { // J-Link supports most ARM and RISC-V targets - target_chip.to_lowercase().contains("stm32") || - target_chip.to_lowercase().contains("nrf") || - target_chip.to_lowercase().contains("cortex") || - target_chip.to_lowercase().contains("risc") + target_chip.to_lowercase().contains("stm32") + || target_chip.to_lowercase().contains("nrf") + || target_chip.to_lowercase().contains("cortex") + || target_chip.to_lowercase().contains("risc") } ProbeType::StLink => { // ST-Link primarily supports STM32 @@ -186,14 +210,14 @@ impl ProbeDiscovery { } ProbeType::DapLink => { // DAPLink supports ARM Cortex targets - target_chip.to_lowercase().contains("cortex") || - target_chip.to_lowercase().contains("stm32") || - target_chip.to_lowercase().contains("nrf") + target_chip.to_lowercase().contains("cortex") + || target_chip.to_lowercase().contains("stm32") + || target_chip.to_lowercase().contains("nrf") } ProbeType::Blackmagic => { // Black Magic Probe supports ARM Cortex - target_chip.to_lowercase().contains("cortex") || - target_chip.to_lowercase().contains("stm32") + target_chip.to_lowercase().contains("cortex") + || target_chip.to_lowercase().contains("stm32") } ProbeType::Ftdi => { // FTDI can support various targets @@ -213,10 +237,22 @@ mod tests { #[test] fn test_probe_type_support() { - assert!(ProbeDiscovery::check_target_support(&ProbeType::JLink, "STM32F407VG")); - assert!(ProbeDiscovery::check_target_support(&ProbeType::StLink, "STM32F407VG")); - assert!(ProbeDiscovery::check_target_support(&ProbeType::DapLink, "nRF52832")); - assert!(!ProbeDiscovery::check_target_support(&ProbeType::StLink, "ESP32")); + assert!(ProbeDiscovery::check_target_support( + &ProbeType::JLink, + "STM32F407VG" + )); + assert!(ProbeDiscovery::check_target_support( + &ProbeType::StLink, + "STM32F407VG" + )); + assert!(ProbeDiscovery::check_target_support( + &ProbeType::DapLink, + "nRF52832" + )); + assert!(!ProbeDiscovery::check_target_support( + &ProbeType::StLink, + "ESP32" + )); } #[tokio::test] @@ -225,7 +261,7 @@ mod tests { // In CI/testing environments, this might be empty let result = ProbeDiscovery::list_probes(); assert!(result.is_ok()); - + let probes = result.unwrap(); // Just verify the structure is correct for probe in probes { @@ -233,4 +269,4 @@ mod tests { assert!(!probe.probe_type.is_empty()); } } -} \ No newline at end of file +} diff --git a/src/debugger/mod.rs b/src/debugger/mod.rs index 2d6f32b..2d19c6a 100644 --- a/src/debugger/mod.rs +++ b/src/debugger/mod.rs @@ -11,4 +11,4 @@ pub struct SessionConfig { pub connect_under_reset: bool, /// Whether to halt after connecting pub halt_after_connect: bool, -} \ No newline at end of file +} diff --git a/src/error.rs b/src/error.rs index e65c1a7..72b5d17 100644 --- a/src/error.rs +++ b/src/error.rs @@ -167,4 +167,4 @@ impl From for DebugError { fn from(error: FlashError) -> Self { DebugError::FlashOperationFailed(error.to_string()) } -} \ No newline at end of file +} diff --git a/src/flash/manager.rs b/src/flash/manager.rs index 44a77f8..3736dc5 100644 --- a/src/flash/manager.rs +++ b/src/flash/manager.rs @@ -1,12 +1,15 @@ //! Flash programming manager - Real probe-rs integration -use crate::error::{Result, DebugError}; +use crate::error::{DebugError, Result}; use std::path::Path; use std::time::Instant; use tracing::{debug, info, warn}; -// Probe-rs imports -use probe_rs::{flashing::{self, FlashProgress}, Session, MemoryInterface}; +// Probe-rs imports +use probe_rs::{ + flashing::{self, FlashProgress}, + MemoryInterface, Session, +}; /// Erase operation types #[derive(Debug, Clone)] @@ -67,18 +70,16 @@ impl FlashManager { } /// Erase flash memory - pub async fn erase_flash( - session: &mut Session, - erase_type: EraseType, - ) -> Result { + pub async fn erase_flash(session: &mut Session, erase_type: EraseType) -> Result { let start_time = Instant::now(); - + match erase_type { EraseType::All => { debug!("Starting full flash erase"); - flashing::erase_all(session, FlashProgress::empty()) - .map_err(|e| DebugError::FlashOperationFailed(format!("Full erase failed: {}", e)))?; - + flashing::erase_all(session, FlashProgress::empty()).map_err(|e| { + DebugError::FlashOperationFailed(format!("Full erase failed: {}", e)) + })?; + info!("Full flash erase completed"); Ok(EraseResult { erase_time_ms: start_time.elapsed().as_millis() as u64, @@ -86,27 +87,9 @@ impl FlashManager { }) } EraseType::Sectors { address, size } => { - debug!("Starting sector erase at 0x{:08X}, size: {} bytes", address, size); - - // Calculate sector range - this is target-specific, using approximation - let sector_size = 4096; // Common sector size, should be target-specific - let sector_count = (size + sector_size - 1) / sector_size; - - // Use probe-rs flashing API for sector erase - let mut core = session.core(0) - .map_err(|e| DebugError::FlashOperationFailed(format!("Failed to get core: {}", e)))?; - - // For now, we'll use memory writes to simulate erase (0xFF) - // Real implementation should use target-specific flash algorithms - let erase_data = vec![0xFFu8; size]; - core.write(address, &erase_data) - .map_err(|e| DebugError::FlashOperationFailed(format!("Sector erase failed: {}", e)))?; - - info!("Sector erase completed: {} sectors", sector_count); - Ok(EraseResult { - erase_time_ms: start_time.elapsed().as_millis() as u64, - sectors_erased: Some(sector_count), - }) + Err(DebugError::FlashOperationFailed(format!( + "Sector erase by address is not implemented safely for probe-rs 0.25 (requested address 0x{address:08X}, size {size} bytes). Use erase_type='all' or add a target-specific sector-index mapping before enabling this operation." + ))) } } } @@ -117,12 +100,16 @@ impl FlashManager { file_path: &Path, format: FileFormat, base_address: Option, + verify: bool, ) -> Result { let start_time = Instant::now(); - + // Check file existence if !file_path.exists() { - return Err(DebugError::FlashOperationFailed(format!("File not found: {}", file_path.display()))); + return Err(DebugError::FlashOperationFailed(format!( + "File not found: {}", + file_path.display() + ))); } debug!("Programming file: {}", file_path.display()); @@ -133,19 +120,29 @@ impl FlashManager { // Auto-detect based on extension match file_path.extension().and_then(|s| s.to_str()) { Some("elf") => flashing::Format::Elf, - Some("hex") => flashing::Format::Hex, - Some("bin") => flashing::Format::Bin(probe_rs::flashing::BinOptions { base_address: None, skip: 0 }), - _ => return Err(DebugError::FlashOperationFailed("Cannot auto-detect file format".to_string())), + Some("hex") => flashing::Format::Hex, + Some("bin") => flashing::Format::Bin(probe_rs::flashing::BinOptions { + base_address, + skip: 0, + }), + _ => { + return Err(DebugError::FlashOperationFailed( + "Cannot auto-detect file format".to_string(), + )) + } } } FileFormat::Elf => flashing::Format::Elf, FileFormat::Hex => flashing::Format::Hex, - FileFormat::Bin => flashing::Format::Bin(probe_rs::flashing::BinOptions { base_address, skip: 0 }), + FileFormat::Bin => flashing::Format::Bin(probe_rs::flashing::BinOptions { + base_address, + skip: 0, + }), }; // Setup download options - use default and override what we need let mut options = flashing::DownloadOptions::default(); - options.verify = true; + options.verify = verify; options.progress = None; // Set base address for BIN files - this might need to be handled differently @@ -161,48 +158,36 @@ impl FlashManager { .map_err(|e| DebugError::FlashOperationFailed(format!("Programming failed: {}", e)))?; let elapsed = start_time.elapsed().as_millis() as u64; - + info!("File programming completed in {}ms", elapsed); - + // Since we can't get exact bytes from probe-rs API, estimate from file size let file_size = std::fs::metadata(file_path) .map(|m| m.len() as usize) .unwrap_or(0); - + Ok(ProgramResult { bytes_programmed: file_size, programming_time_ms: elapsed, - verification_result: Some(true), // probe-rs handles verification internally + verification_result: verify.then_some(true), }) } /// Program binary data to flash pub async fn program_data( - session: &mut Session, + _session: &mut Session, data: &[u8], base_address: u64, ) -> Result { - let start_time = Instant::now(); - - debug!("Programming {} bytes to address 0x{:08X}", data.len(), base_address); - - // Use direct memory write for now - FlashLoader API requires memory map - let mut core = session.core(0) - .map_err(|e| DebugError::FlashOperationFailed(format!("Failed to get core: {}", e)))?; - - // Write data directly to flash memory - core.write(base_address, data) - .map_err(|e| DebugError::FlashOperationFailed(format!("Failed to write data: {}", e)))?; - - let elapsed = start_time.elapsed().as_millis() as u64; - - info!("Data programming completed: {} bytes in {}ms", data.len(), elapsed); - - Ok(ProgramResult { - bytes_programmed: data.len(), - programming_time_ms: elapsed, - verification_result: None, // Manual verification needed - }) + debug!( + "Rejected direct flash data programming: {} bytes to address 0x{:08X}", + data.len(), + base_address + ); + Err(DebugError::FlashOperationFailed( + "Direct flash programming by memory write is disabled. Program ELF/HEX/BIN files through probe-rs flash algorithms instead." + .to_string(), + )) } /// Verify flash contents @@ -211,15 +196,21 @@ impl FlashManager { expected_data: &[u8], address: u64, ) -> Result { - debug!("Verifying {} bytes at address 0x{:08X}", expected_data.len(), address); - - let mut core = session.core(0) + debug!( + "Verifying {} bytes at address 0x{:08X}", + expected_data.len(), + address + ); + + let mut core = session + .core(0) .map_err(|e| DebugError::FlashOperationFailed(format!("Failed to get core: {}", e)))?; - + // Read actual data from flash let mut actual_data = vec![0u8; expected_data.len()]; - core.read(address, &mut actual_data) - .map_err(|e| DebugError::FlashOperationFailed(format!("Failed to read flash: {}", e)))?; + core.read(address, &mut actual_data).map_err(|e| { + DebugError::FlashOperationFailed(format!("Failed to read flash: {}", e)) + })?; // Compare data and find mismatches let mut mismatches = Vec::new(); @@ -234,9 +225,12 @@ impl FlashManager { } let success = mismatches.is_empty(); - + if success { - info!("Flash verification successful: {} bytes", expected_data.len()); + info!( + "Flash verification successful: {} bytes", + expected_data.len() + ); } else { warn!("Flash verification failed: {} mismatches", mismatches.len()); } @@ -253,4 +247,4 @@ impl Default for FlashManager { fn default() -> Self { Self::new() } -} \ No newline at end of file +} diff --git a/src/flash/mod.rs b/src/flash/mod.rs index 68c772f..38b48ad 100644 --- a/src/flash/mod.rs +++ b/src/flash/mod.rs @@ -3,11 +3,5 @@ pub mod manager; pub use manager::{ - FlashManager, - EraseType, - FileFormat, - EraseResult, - ProgramResult, - VerifyResult, - VerifyMismatch -}; \ No newline at end of file + EraseResult, EraseType, FileFormat, FlashManager, ProgramResult, VerifyMismatch, VerifyResult, +}; diff --git a/src/lib.rs b/src/lib.rs index 7e287f6..13c336a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,17 @@ //! Embedded Debugger MCP Server -//! +//! //! A Model Context Protocol server for embedded debugging using probe-rs. -//! Provides AI assistants with comprehensive debugging capabilities for +//! Provides AI assistants with debugging and flash programming tools for //! embedded systems including ARM Cortex-M, RISC-V, J-Link, DAPLink, ST-Link, and other debug probes. pub mod config; -pub mod error; -pub mod utils; pub mod debugger; -pub mod rtt; +pub mod error; pub mod flash; +pub mod rtt; pub mod tools; +pub mod utils; -pub use error::{DebugError, Result}; pub use config::Config; -pub use tools::EmbeddedDebuggerToolHandler; \ No newline at end of file +pub use error::{DebugError, Result}; +pub use tools::EmbeddedDebuggerToolHandler; diff --git a/src/main.rs b/src/main.rs index 6fc5a0d..ab6f0bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,20 @@ //! Embedded Debugger MCP Server - Main Entry Point use clap::Parser; -use tracing::{info, error, debug}; -use tracing_subscriber::{EnvFilter, fmt}; -use rmcp::{ServiceExt, transport::stdio}; - -use embedded_debugger_mcp::{ - Config, - config::Args, - tools::EmbeddedDebuggerToolHandler, +use embedded_debugger_mcp::config::{ + Command as CliCommand, ConfigCommand, ProbeCommand, SkillCommand, }; +use embedded_debugger_mcp::debugger::discovery::ProbeDiscovery; +use rmcp::{transport::stdio, ServiceExt}; +use serde::Serialize; +use std::process::Command as ProcessCommand; +use tracing::{debug, error, info}; +use tracing_subscriber::{fmt, EnvFilter}; + +use embedded_debugger_mcp::{config::Args, tools::EmbeddedDebuggerToolHandler, Config}; + +const DEFAULT_SKILL_PROMPT: &str = + include_str!("../skills/embedded-debugger/references/default-prompt.md"); #[tokio::main] async fn main() -> Result<(), Box> { @@ -17,7 +22,14 @@ async fn main() -> Result<(), Box> { let args = Args::parse(); // Handle special flags first - if args.generate_config { + if args.generate_config + || matches!( + args.command, + Some(CliCommand::Config { + action: ConfigCommand::Generate + }) + ) + { let config = Config::default(); println!("{}", config.to_toml()?); return Ok(()); @@ -26,15 +38,17 @@ async fn main() -> Result<(), Box> { // Initialize logging init_logging(&args)?; - info!("Starting Debugger MCP Server v{}", env!("CARGO_PKG_VERSION")); + info!( + "Starting Debugger MCP Server v{}", + env!("CARGO_PKG_VERSION") + ); debug!("Command line args: {:?}", args); // Load configuration - let mut config = Config::load(args.config.as_ref()) - .map_err(|e| { - error!("Failed to load configuration: {}", e); - e - })?; + let mut config = Config::load(args.config.as_ref()).map_err(|e| { + error!("Failed to load configuration: {}", e); + e + })?; // Merge command line arguments into configuration config.merge_args(&args); @@ -51,22 +65,100 @@ async fn main() -> Result<(), Box> { } // Validate final configuration - config.validate() - .map_err(|e| { - error!("Configuration validation failed: {}", e); - e - })?; + config.validate().map_err(|e| { + error!("Configuration validation failed: {}", e); + e + })?; info!("Configuration loaded and validated successfully"); + if let Some(command) = args.command.clone() { + return run_cli_command(command, config).await; + } + + run_mcp_server(config).await +} + +async fn run_cli_command( + command: CliCommand, + config: Config, +) -> Result<(), Box> { + match command { + CliCommand::Serve => run_mcp_server(config).await, + CliCommand::Config { action } => { + match action { + ConfigCommand::Generate => println!("{}", Config::default().to_toml()?), + ConfigCommand::Validate => println!("Configuration is valid"), + ConfigCommand::Show => println!("{}", config.to_toml()?), + } + Ok(()) + } + CliCommand::Probes { + action: ProbeCommand::List { json }, + } => { + let probes = ProbeDiscovery::list_probes()?; + if json { + println!("{}", serde_json::to_string_pretty(&probes)?); + } else if probes.is_empty() { + println!("No debug probes found"); + } else { + for (index, probe) in probes.iter().enumerate() { + println!( + "{}. {} ({:04X}:{:04X}) {}", + index + 1, + probe.identifier, + probe.vendor_id, + probe.product_id, + probe.serial_number.as_deref().unwrap_or("no serial") + ); + } + } + Ok(()) + } + CliCommand::Doctor { json } => { + let report = DoctorReport::collect(&config); + if json { + println!("{}", serde_json::to_string_pretty(&report)?); + } else { + println!("embedded-debugger-mcp doctor"); + println!("version: {}", report.version); + println!( + "rustc: {}", + report.rustc_version.as_deref().unwrap_or("not found") + ); + println!("config_valid: {}", report.config_valid); + if let Some(error) = &report.config_error { + println!("config_error: {}", error); + } + println!("probe_count: {}", report.probe_count.unwrap_or(0)); + if let Some(error) = &report.probe_error { + println!("probe_error: {}", error); + } + println!("mcp_mode: use `embedded-debugger-mcp serve`"); + println!("cli_skill_mode: use `embedded-debugger-mcp skill print-prompt`"); + } + Ok(()) + } + CliCommand::Skill { + action: SkillCommand::PrintPrompt, + } => { + print!("{}", DEFAULT_SKILL_PROMPT); + Ok(()) + } + } +} + +async fn run_mcp_server(config: Config) -> Result<(), Box> { // Create and serve the handler using rust-sdk standard pattern - let service = EmbeddedDebuggerToolHandler::new(config.server.max_sessions) - .serve(stdio()).await.inspect_err(|e| { + let service = EmbeddedDebuggerToolHandler::new(config) + .serve(stdio()) + .await + .inspect_err(|e| { error!("Serving error: {:?}", e); })?; - + info!("Embedded Debugger MCP Server started successfully"); - + // Wait for the service to complete service.waiting().await?; @@ -77,10 +169,46 @@ async fn main() -> Result<(), Box> { Ok(()) } +#[derive(Debug, Serialize)] +struct DoctorReport { + version: &'static str, + rustc_version: Option, + config_valid: bool, + config_error: Option, + probe_count: Option, + probe_error: Option, +} + +impl DoctorReport { + fn collect(config: &Config) -> Self { + let config_result = config.validate(); + let probe_result = ProbeDiscovery::list_probes(); + + Self { + version: env!("CARGO_PKG_VERSION"), + rustc_version: command_output("rustc", &["--version"]), + config_valid: config_result.is_ok(), + config_error: config_result.err().map(|error| error.to_string()), + probe_count: probe_result.as_ref().ok().map(Vec::len), + probe_error: probe_result.err().map(|error| error.to_string()), + } + } +} + +fn command_output(command: &str, args: &[&str]) -> Option { + let output = ProcessCommand::new(command).args(args).output().ok()?; + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8(output.stdout).ok()?; + Some(stdout.trim().to_string()) +} + /// Initialize logging system fn init_logging(args: &Args) -> Result<(), Box> { - let env_filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new(&args.log_level)); + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)); let subscriber = fmt::Subscriber::builder() .with_env_filter(env_filter) @@ -95,37 +223,46 @@ fn init_logging(args: &Args) -> Result<(), Box> { .create(true) .append(true) .open(log_file)?; - - subscriber - .with_writer(file) - .init(); - + + subscriber.with_writer(file).init(); + println!("Logging to file: {}", log_file.display()); } else { - subscriber - .with_writer(std::io::stderr) - .init(); + subscriber.with_writer(std::io::stderr).init(); } debug!("Logging initialized with level: {}", args.log_level); Ok(()) } - #[cfg(test)] mod tests { use super::*; #[test] fn test_args_parsing() { - let args = Args::parse_from(&[ - "debugger-mcp-rs", - "--log-level", "debug", - "--max-sessions", "10", + let args = Args::parse_from([ + "embedded-debugger-mcp", + "--log-level", + "debug", + "--max-sessions", + "10", ]); - + assert_eq!(args.log_level, "debug"); assert_eq!(args.max_sessions, 10); + assert!(args.command.is_none()); + } + + #[test] + fn test_subcommand_parsing() { + let args = Args::parse_from(["embedded-debugger-mcp", "probes", "list", "--json"]); + assert_eq!( + args.command, + Some(CliCommand::Probes { + action: ProbeCommand::List { json: true } + }) + ); } #[test] @@ -135,5 +272,4 @@ mod tests { assert_eq!(config.server.max_sessions, 5); assert_eq!(config.debugger.default_speed_khz, 4000); } - -} \ No newline at end of file +} diff --git a/src/rtt/elf_parser.rs b/src/rtt/elf_parser.rs index 8e4a84a..29b0e02 100644 --- a/src/rtt/elf_parser.rs +++ b/src/rtt/elf_parser.rs @@ -12,34 +12,55 @@ const RTT_SYMBOL_NAME: &str = "_SEGGER_RTT"; /// This is the primary method used by probe-rs for RTT detection pub fn get_rtt_symbol_from_elf(elf_path: &Path) -> Result { debug!("Parsing ELF file for RTT symbol: {}", elf_path.display()); - + // Read ELF file let elf_data = std::fs::read(elf_path).map_err(|e| { - DebugError::RttError(format!("Failed to read ELF file {}: {}", elf_path.display(), e)) + DebugError::RttError(format!( + "Failed to read ELF file {}: {}", + elf_path.display(), + e + )) })?; - + // Parse ELF structure let elf = goblin::elf::Elf::parse(&elf_data).map_err(|e| { - DebugError::RttError(format!("Failed to parse ELF file {}: {}", elf_path.display(), e)) + DebugError::RttError(format!( + "Failed to parse ELF file {}: {}", + elf_path.display(), + e + )) })?; - - info!("ELF file parsed successfully, searching for {} symbol", RTT_SYMBOL_NAME); - debug!("ELF info - entry: 0x{:08X}, symbols: {}", elf.entry, elf.syms.len()); - + + info!( + "ELF file parsed successfully, searching for {} symbol", + RTT_SYMBOL_NAME + ); + debug!( + "ELF info - entry: 0x{:08X}, symbols: {}", + elf.entry, + elf.syms.len() + ); + // Search for _SEGGER_RTT symbol in symbol table for sym in elf.syms.iter() { if let Some(name) = elf.strtab.get_at(sym.st_name) { debug!("Found symbol: {} at 0x{:08X}", name, sym.st_value); - + if name == RTT_SYMBOL_NAME { let rtt_address = sym.st_value; - info!("✅ Found {} symbol at address 0x{:08X}", RTT_SYMBOL_NAME, rtt_address); - + info!( + "Found {} symbol at address 0x{:08X}", + RTT_SYMBOL_NAME, rtt_address + ); + // Validate address is reasonable (should be in RAM) if is_valid_rtt_address(rtt_address) { return Ok(rtt_address); } else { - warn!("RTT symbol address 0x{:08X} appears invalid (not in typical RAM range)", rtt_address); + warn!( + "RTT symbol address 0x{:08X} appears invalid (not in typical RAM range)", + rtt_address + ); return Err(DebugError::RttError(format!( "RTT symbol found at invalid address 0x{:08X} (expected in RAM range 0x20000000-0x2FFFFFFF)", rtt_address @@ -48,12 +69,16 @@ pub fn get_rtt_symbol_from_elf(elf_path: &Path) -> Result { } } } - + // Symbol not found - debug!("RTT symbol search completed, {} not found in {} symbols", RTT_SYMBOL_NAME, elf.syms.len()); + debug!( + "RTT symbol search completed, {} not found in {} symbols", + RTT_SYMBOL_NAME, + elf.syms.len() + ); Err(DebugError::RttError(format!( "{} symbol not found in ELF file {}. Firmware may not have RTT enabled or symbols may be stripped.", - RTT_SYMBOL_NAME, + RTT_SYMBOL_NAME, elf_path.display() ))) } @@ -66,20 +91,18 @@ fn is_valid_rtt_address(address: u64) -> bool { // But allow broader range for different MCUs const RAM_START: u64 = 0x20000000; const RAM_END: u64 = 0x2FFFFFFF; - - address >= RAM_START && address <= RAM_END + + (RAM_START..=RAM_END).contains(&address) } -/// Get comprehensive ELF information for debugging +/// Get ELF information used for RTT debugging pub fn get_elf_debug_info(elf_path: &Path) -> Result { - let elf_data = std::fs::read(elf_path).map_err(|e| { - DebugError::RttError(format!("Failed to read ELF file: {}", e)) - })?; - - let elf = goblin::elf::Elf::parse(&elf_data).map_err(|e| { - DebugError::RttError(format!("Failed to parse ELF file: {}", e)) - })?; - + let elf_data = std::fs::read(elf_path) + .map_err(|e| DebugError::RttError(format!("Failed to read ELF file: {}", e)))?; + + let elf = goblin::elf::Elf::parse(&elf_data) + .map_err(|e| DebugError::RttError(format!("Failed to parse ELF file: {}", e)))?; + let mut debug_symbols = Vec::new(); for sym in elf.syms.iter() { if let Some(name) = elf.strtab.get_at(sym.st_name) { @@ -92,7 +115,7 @@ pub fn get_elf_debug_info(elf_path: &Path) -> Result { } } } - + Ok(ElfDebugInfo { entry_point: elf.entry, symbol_count: elf.syms.len(), @@ -119,17 +142,17 @@ pub struct SymbolInfo { #[cfg(test)] mod tests { use super::*; - + #[test] fn test_valid_rtt_address() { // Valid STM32G4 RAM addresses assert!(is_valid_rtt_address(0x20000000)); // Start of SRAM1 assert!(is_valid_rtt_address(0x20008000)); // Start of SRAM2 assert!(is_valid_rtt_address(0x2000A000)); // End of SRAM2 - + // Invalid addresses assert!(!is_valid_rtt_address(0x08000000)); // Flash assert!(!is_valid_rtt_address(0x00000000)); // Null assert!(!is_valid_rtt_address(0x40000000)); // Peripherals } -} \ No newline at end of file +} diff --git a/src/rtt/manager.rs b/src/rtt/manager.rs index 04d30d4..0e0623b 100644 --- a/src/rtt/manager.rs +++ b/src/rtt/manager.rs @@ -1,12 +1,15 @@ //! RTT manager implementation using probe-rs RTT API use crate::error::{DebugError, Result}; +use probe_rs::{ + rtt::{Rtt, ScanRegion}, + MemoryInterface, Session, +}; use std::collections::HashMap; -use std::sync::Arc; use std::path::Path; +use std::sync::Arc; use tokio::sync::Mutex; -use tracing::{debug, info, error, warn}; -use probe_rs::{Session, rtt::{Rtt, ScanRegion}, MemoryInterface}; +use tracing::{debug, error, info, warn}; /// RTT manager for hardware communication with embedded targets #[derive(Debug)] @@ -72,16 +75,22 @@ impl RttManager { // Phase 1: Try ELF symbol detection (primary method) match crate::rtt::elf_parser::get_rtt_symbol_from_elf(firmware_path) { Ok(symbol_addr) => { - info!("✅ Found _SEGGER_RTT symbol at 0x{:08X}, attempting direct connection", symbol_addr); - + info!( + "Found _SEGGER_RTT symbol at 0x{:08X}, attempting direct connection", + symbol_addr + ); + // Try direct connection at symbol address match self.try_rtt_at_address(session.clone(), symbol_addr).await { Ok(_) => { - info!("🎯 RTT connected successfully using ELF symbol address!"); + info!("RTT connected successfully using ELF symbol address!"); return Ok(()); } Err(e) => { - warn!("RTT connection failed at symbol address 0x{:08X}: {}", symbol_addr, e); + warn!( + "RTT connection failed at symbol address 0x{:08X}: {}", + symbol_addr, e + ); info!("Falling back to memory scanning..."); } } @@ -103,7 +112,10 @@ impl RttManager { session: Arc>, address: u64, ) -> Result<()> { - debug!("Attempting RTT connection at specific address: 0x{:08X}", address); + debug!( + "Attempting RTT connection at specific address: 0x{:08X}", + address + ); // Store session reference self.session = Some(session.clone()); @@ -122,17 +134,20 @@ impl RttManager { address ))); } - debug!("✅ RTT control block validated at 0x{:08X}", address); + debug!("RTT control block validated at 0x{:08X}", address); // CRITICAL FIX: Use ScanRegion::Exact for direct address connection debug!("Using ScanRegion::Exact for direct address connection..."); - + let scan_region = ScanRegion::Exact(address); let rtt_result = Rtt::attach_region(&mut core, &scan_region); match rtt_result { Ok(rtt) => { - info!("Successfully attached RTT at ELF symbol address 0x{:08X}!", address); + info!( + "Successfully attached RTT at ELF symbol address 0x{:08X}!", + address + ); self.complete_attachment_sync(rtt) } Err(e) => { @@ -156,7 +171,10 @@ impl RttManager { // Read 16 bytes for RTT magic identifier let mut id_buffer = [0u8; 16]; core.read(address, &mut id_buffer).map_err(|e| { - DebugError::RttError(format!("Failed to read RTT control block at 0x{:08X}: {}", address, e)) + DebugError::RttError(format!( + "Failed to read RTT control block at 0x{:08X}: {}", + address, e + )) })?; // Check for "SEGGER RTT" magic identifier @@ -164,9 +182,13 @@ impl RttManager { let is_valid = id_buffer == RTT_ID; if is_valid { - debug!("✅ Valid RTT control block found at 0x{:08X}", address); + debug!("Valid RTT control block found at 0x{:08X}", address); } else { - debug!("❌ Invalid RTT control block at 0x{:08X}, found: {:02X?}", address, &id_buffer[..10]); + debug!( + "Invalid RTT control block at 0x{:08X}, found: {:02X?}", + address, + &id_buffer[..10] + ); } Ok(is_valid) @@ -175,51 +197,49 @@ impl RttManager { /// Attach to RTT on target using probe-rs RTT API with enhanced detection /// Priority: ELF symbol detection first, then memory scanning fallback pub async fn attach( - &mut self, + &mut self, session: Arc>, control_block_address: Option, - memory_ranges: Option> + memory_ranges: Option>, ) -> Result<()> { debug!("Attaching to RTT using probe-rs integration with enhanced detection"); - + // Store session reference self.session = Some(session.clone()); - + // Note: memory_map not needed for probe-rs 0.25 attach_region API - + // Get the session and core to perform RTT attachment let mut session_guard = session.lock().await; let mut core = session_guard.core(0).map_err(|e| { error!("Failed to get core for RTT attachment: {}", e); DebugError::RttError(format!("Failed to get core: {}", e)) })?; - + // Check if target is running (important for RTT initialization) let core_status = core.status().map_err(|e| { error!("Failed to get core status: {}", e); DebugError::RttError(format!("Failed to get core status: {}", e)) })?; debug!("Core status before RTT attach: {:?}", core_status); - + // Build ScanRegion based on parameters let scan_region = if let Some(cb_addr) = control_block_address { info!("RTT scan: Using exact address: 0x{:08X}", cb_addr); ScanRegion::Exact(cb_addr) } else if let Some(ranges) = memory_ranges { info!("RTT scan: Using custom memory ranges: {:?}", ranges); - let ranges = ranges.into_iter() - .map(|(start, end)| start..end) - .collect(); + let ranges = ranges.into_iter().map(|(start, end)| start..end).collect(); ScanRegion::Ranges(ranges) } else { info!("RTT scan: Using RAM scan (probe-rs default)"); ScanRegion::Ram }; - + // Try RTT attachment with appropriate scan region debug!("Attempting RTT attach with scan region: {:?}", scan_region); let rtt_result = Rtt::attach_region(&mut core, &scan_region); - + match rtt_result { Ok(rtt) => { info!("Successfully attached to RTT control block!"); @@ -227,7 +247,7 @@ impl RttManager { } Err(e) => { error!("RTT attachment failed: {}", e); - + // Provide detailed debugging information let detailed_error = format!( "RTT attachment failed: {}\n\n\ @@ -240,23 +260,20 @@ impl RttManager { - Ensure target is running (not halted) during RTT initialization\n\ - Check that firmware has sufficient time to initialize RTT\n\ - Verify memory regions contain RTT control block\n\ - - For defmt: ensure defmt-rtt feature is enabled in firmware", - e, - core_status, - scan_region, - control_block_address + - For defmt: ensure defmt-rtt feature is enabled in firmware", + e, core_status, scan_region, control_block_address ); - + Err(DebugError::RttError(detailed_error)) } } } - - /// Complete RTT attachment by discovering channels (synchronous version) + + /// Finish RTT attachment by discovering channels (synchronous version) fn complete_attachment_sync(&mut self, mut rtt: Rtt) -> Result<()> { // Clear any previous state self.channels.clear(); - + // Discover up channels (target to host) let up_channels = rtt.up_channels(); self.up_channel_count = up_channels.len(); @@ -270,11 +287,15 @@ impl RttManager { buffer_size: up_channel.buffer_size(), }; self.channels.insert(i as u32, channel_info); - debug!("Discovered up channel {}: {} (size: {} bytes)", - i, up_channel.name().unwrap_or("unnamed"), up_channel.buffer_size()); + debug!( + "Discovered up channel {}: {} (size: {} bytes)", + i, + up_channel.name().unwrap_or("unnamed"), + up_channel.buffer_size() + ); } } - + // Discover down channels (host to target) let down_channels = rtt.down_channels(); self.down_channel_count = down_channels.len(); @@ -282,67 +303,109 @@ impl RttManager { if let Some(down_channel) = down_channels.get(i) { let channel_info = ChannelInfo { id: i as u32, - name: down_channel.name().unwrap_or(&format!("Down{}", i)).to_string(), + name: down_channel + .name() + .unwrap_or(&format!("Down{}", i)) + .to_string(), direction: ChannelDirection::Down, mode: "RTT".to_string(), // Simplified as mode() requires &mut Core buffer_size: down_channel.buffer_size(), }; // Use offset for down channels to avoid ID conflicts self.channels.insert(1000 + i as u32, channel_info); - debug!("Discovered down channel {}: {} (size: {} bytes)", - i, down_channel.name().unwrap_or("unnamed"), down_channel.buffer_size()); + debug!( + "Discovered down channel {}: {} (size: {} bytes)", + i, + down_channel.name().unwrap_or("unnamed"), + down_channel.buffer_size() + ); } } - + // Store the RTT instance self.rtt = Some(rtt); self.attached = true; - - info!("RTT attachment completed: {} up channels, {} down channels", - self.up_channel_count, self.down_channel_count); + + info!( + "RTT attachment completed: {} up channels, {} down channels", + self.up_channel_count, self.down_channel_count + ); Ok(()) } /// Detach from RTT pub async fn detach(&mut self) -> Result<()> { debug!("Detaching from RTT"); - + self.attached = false; self.rtt = None; self.session = None; self.channels.clear(); self.up_channel_count = 0; self.down_channel_count = 0; - + info!("RTT detached successfully"); Ok(()) } /// Read from RTT up channel using probe-rs RTT API - pub async fn read_channel(&mut self, channel: u32) -> Result> { + pub async fn read_channel( + &mut self, + channel: u32, + max_bytes: usize, + timeout_ms: u64, + ) -> Result> { + if max_bytes == 0 { + return Ok(Vec::new()); + } + + let started = tokio::time::Instant::now(); + let timeout = tokio::time::Duration::from_millis(timeout_ms); + + loop { + let data = self.read_channel_once(channel, max_bytes).await?; + if !data.is_empty() || timeout_ms == 0 || started.elapsed() >= timeout { + return Ok(data); + } + + let remaining = timeout.saturating_sub(started.elapsed()); + let sleep_for = remaining.min(tokio::time::Duration::from_millis(10)); + if sleep_for.is_zero() { + return Ok(Vec::new()); + } + tokio::time::sleep(sleep_for).await; + } + } + + async fn read_channel_once(&mut self, channel: u32, max_bytes: usize) -> Result> { if !self.attached { return Err(DebugError::RttError("RTT not attached".to_string())); } - let session = self.session.as_ref() + let session = self + .session + .as_ref() .ok_or_else(|| DebugError::RttError("No session available".to_string()))?; - - let rtt = self.rtt.as_mut() + + let rtt = self + .rtt + .as_mut() .ok_or_else(|| DebugError::RttError("No RTT instance available".to_string()))?; - + // Lock session and get core let mut session_guard = session.lock().await; - let mut core = session_guard.core(0).map_err(|e| { - DebugError::RttError(format!("Failed to get core: {}", e)) - })?; - + let mut core = session_guard + .core(0) + .map_err(|e| DebugError::RttError(format!("Failed to get core: {}", e)))?; + // Get the up channel (mutable reference) let up_channels = rtt.up_channels(); - let up_channel = up_channels.get_mut(channel as usize) + let up_channel = up_channels + .get_mut(channel as usize) .ok_or_else(|| DebugError::RttError(format!("Up channel {} not found", channel)))?; - + // Read from RTT channel - let mut buffer = vec![0u8; 1024]; // Buffer for reading + let mut buffer = vec![0u8; max_bytes]; match up_channel.read(&mut core, &mut buffer) { Ok(bytes_read) => { buffer.truncate(bytes_read); @@ -364,28 +427,40 @@ impl RttManager { return Err(DebugError::RttError("RTT not attached".to_string())); } - let session = self.session.as_ref() + let session = self + .session + .as_ref() .ok_or_else(|| DebugError::RttError("No session available".to_string()))?; - - let rtt = self.rtt.as_mut() + + let rtt = self + .rtt + .as_mut() .ok_or_else(|| DebugError::RttError("No RTT instance available".to_string()))?; - + // Lock session and get core let mut session_guard = session.lock().await; - let mut core = session_guard.core(0).map_err(|e| { - DebugError::RttError(format!("Failed to get core: {}", e)) - })?; - + let mut core = session_guard + .core(0) + .map_err(|e| DebugError::RttError(format!("Failed to get core: {}", e)))?; + // Get the down channel (mutable reference) let down_channels = rtt.down_channels(); - let down_channel = down_channels.get_mut(channel as usize) + let down_channel = down_channels + .get_mut(channel as usize) .ok_or_else(|| DebugError::RttError(format!("Down channel {} not found", channel)))?; - + // Write to RTT channel match down_channel.write(&mut core, data) { Ok(bytes_written) => { - debug!("Wrote {} bytes to RTT down channel {}", bytes_written, channel); - info!("RTT Write Channel {}: {:?}", channel, String::from_utf8_lossy(&data[..bytes_written])); + debug!( + "Wrote {} bytes to RTT down channel {}", + bytes_written, channel + ); + info!( + "RTT Write Channel {}: {:?}", + channel, + String::from_utf8_lossy(&data[..bytes_written]) + ); Ok(bytes_written) } Err(e) => { @@ -414,4 +489,4 @@ impl RttManager { pub fn down_channel_count(&self) -> usize { self.down_channel_count } -} \ No newline at end of file +} diff --git a/src/rtt/mod.rs b/src/rtt/mod.rs index b8792bc..5d59e55 100644 --- a/src/rtt/mod.rs +++ b/src/rtt/mod.rs @@ -1,11 +1,11 @@ //! RTT (Real-Time Transfer) communication -//! +//! //! This module provides RTT integration using probe-rs for embedded debugging //! with enhanced ELF symbol detection based on probe-rs implementation analysis. -pub mod manager; pub mod elf_parser; +pub mod manager; // Export RTT components -pub use manager::{RttManager, ChannelInfo, ChannelDirection}; -pub use elf_parser::{get_rtt_symbol_from_elf, get_elf_debug_info, ElfDebugInfo, SymbolInfo}; \ No newline at end of file +pub use elf_parser::{get_elf_debug_info, get_rtt_symbol_from_elf, ElfDebugInfo, SymbolInfo}; +pub use manager::{ChannelDirection, ChannelInfo, RttManager}; diff --git a/src/tools/debugger_tools.rs b/src/tools/debugger_tools.rs index c6dd9ca..fa9c31d 100644 --- a/src/tools/debugger_tools.rs +++ b/src/tools/debugger_tools.rs @@ -1,31 +1,30 @@ -//! Complete RMCP 0.3.2 implementation for embedded debugger MCP tools -//! +//! RMCP 0.3.2 implementation for embedded debugger MCP tools +//! //! This implementation provides all 18 debugging tools (13 base + 5 RTT) using real probe-rs integration use rmcp::{ - tool, tool_handler, tool_router, ServerHandler, handler::server::{router::tool::ToolRouter, tool::Parameters}, model::*, - ErrorData as McpError, service::RequestContext, - RoleServer, + tool, tool_handler, tool_router, ErrorData as McpError, RoleServer, ServerHandler, }; -use tracing::{debug, error, info, warn}; -use std::future::Future; use std::collections::HashMap; +use std::future::Future; +use std::path::{Path, PathBuf}; use std::sync::Arc; -use tokio::sync::RwLock; +use tokio::sync::{OwnedSemaphorePermit, RwLock, Semaphore}; +use tracing::{debug, error, info, warn}; use super::types::*; // Flash types will be used through crate::flash:: prefix +use crate::config::{Config, TargetConfig}; use crate::rtt::RttManager; // Probe-rs imports use probe_rs::probe::list::Lister; -use probe_rs::{Session, Permissions, CoreStatus, MemoryInterface, RegisterValue}; +use probe_rs::{CoreStatus, MemoryInterface, Permissions, RegisterValue, Session}; /// Debug session information -#[derive(Debug)] pub struct DebugSession { pub session_id: String, pub probe_identifier: String, @@ -33,23 +32,242 @@ pub struct DebugSession { pub created_at: chrono::DateTime, pub session: Arc>, pub rtt_manager: Arc>, + _session_slot: OwnedSemaphorePermit, } -/// Complete embedded debugger tool handler with all 18 tools +/// Embedded debugger tool handler with debug, RTT, and flash tools #[derive(Clone)] pub struct EmbeddedDebuggerToolHandler { #[allow(dead_code)] tool_router: ToolRouter, sessions: Arc>>>, + config: Arc, max_sessions: usize, + session_slots: Arc, } impl EmbeddedDebuggerToolHandler { - pub fn new(max_sessions: usize) -> Self { + pub fn new(config: impl Into) -> Self { + let config = config.into(); + let max_sessions = config.server.max_sessions; Self { tool_router: Self::tool_router(), sessions: Arc::new(RwLock::new(HashMap::new())), + config: Arc::new(config), max_sessions, + session_slots: Arc::new(Semaphore::new(max_sessions)), + } + } + + async fn get_session(&self, session_id: &str) -> Result, McpError> { + let sessions = self.sessions.read().await; + sessions.get(session_id).cloned().ok_or_else(|| { + McpError::internal_error( + format!( + "Session '{}' not found. Use 'connect' to establish a debug session first.", + session_id + ), + None, + ) + }) + } + + fn flash_erase_allowed(&self) -> bool { + self.config.security.allow_flash_erase || self.config.flash.allow_erase + } + + fn ensure_flash_erase_allowed(&self) -> Result<(), McpError> { + if self.flash_erase_allowed() { + Ok(()) + } else { + Err(McpError::internal_error( + "Flash erase is disabled by configuration. Enable security.allow_flash_erase or flash.allow_erase to use this operation." + .to_string(), + None, + )) + } + } + + fn ensure_memory_read_allowed( + &self, + session: &DebugSession, + address: u64, + size: usize, + ) -> Result<(), McpError> { + if size == 0 { + return Err(McpError::internal_error( + "Memory read size must be greater than zero.".to_string(), + None, + )); + } + if size > self.config.memory.max_read_size { + return Err(McpError::internal_error( + format!( + "Memory read size {} exceeds configured limit {}.", + size, self.config.memory.max_read_size + ), + None, + )); + } + self.ensure_memory_region_allowed(session, address, size, 'r') + } + + fn ensure_memory_write_allowed( + &self, + session: &DebugSession, + address: u64, + size: usize, + ) -> Result<(), McpError> { + if !self.config.security.allow_memory_write { + return Err(McpError::internal_error( + "Memory writes are disabled by configuration.".to_string(), + None, + )); + } + if size == 0 { + return Err(McpError::internal_error( + "Memory write size must be greater than zero.".to_string(), + None, + )); + } + if size > self.config.memory.max_write_size { + return Err(McpError::internal_error( + format!( + "Memory write size {} exceeds configured limit {}.", + size, self.config.memory.max_write_size + ), + None, + )); + } + self.ensure_memory_region_allowed(session, address, size, 'w') + } + + fn ensure_memory_region_allowed( + &self, + session: &DebugSession, + address: u64, + size: usize, + required_access: char, + ) -> Result<(), McpError> { + let end_exclusive = address.checked_add(size as u64).ok_or_else(|| { + McpError::internal_error( + "Memory range overflows u64 address space.".to_string(), + None, + ) + })?; + + if !self.config.security.restrict_memory_access { + return Ok(()); + } + + let target = self + .target_config_for(&session.target_chip) + .ok_or_else(|| { + McpError::internal_error( + format!( + "Memory access is restricted, but target '{}' has no configured memory map.", + session.target_chip + ), + None, + ) + })?; + + let last_address = end_exclusive - 1; + let allowed = target.memory_regions.iter().any(|region| { + address >= region.start + && last_address <= region.end + && region.access.contains(required_access) + }); + + if allowed { + Ok(()) + } else { + Err(McpError::internal_error( + format!( + "Memory range 0x{address:08X}..0x{last_address:08X} is outside configured '{}' access regions for target '{}'.", + required_access, session.target_chip + ), + None, + )) + } + } + + fn target_config_for(&self, target_chip: &str) -> Option<&TargetConfig> { + let target_chip_lower = target_chip.to_lowercase(); + self.config + .targets + .get(&target_chip_lower) + .or_else(|| self.config.targets.get(target_chip)) + .or_else(|| { + self.config.targets.values().find(|target| { + target.chip.eq_ignore_ascii_case(target_chip) + || target.name.eq_ignore_ascii_case(target_chip) + }) + }) + } + + fn resolve_allowed_file_path(&self, path: &str, max_size: usize) -> Result { + let path = Path::new(path); + let canonical = path.canonicalize().map_err(|e| { + McpError::internal_error( + format!("Failed to resolve file path '{}': {}", path.display(), e), + None, + ) + })?; + + let metadata = canonical.metadata().map_err(|e| { + McpError::internal_error( + format!( + "Failed to read metadata for '{}': {}", + canonical.display(), + e + ), + None, + ) + })?; + if !metadata.is_file() { + return Err(McpError::internal_error( + format!("Path '{}' is not a regular file.", canonical.display()), + None, + )); + } + + let file_size = metadata.len() as usize; + let max_size = max_size.min(self.config.security.max_file_size); + if file_size > max_size { + return Err(McpError::internal_error( + format!( + "File '{}' is {} bytes, exceeding configured limit {}.", + canonical.display(), + file_size, + max_size + ), + None, + )); + } + + if self.config.security.allowed_file_paths.is_empty() { + return Ok(canonical); + } + + let allowed = self + .config + .security + .allowed_file_paths + .iter() + .filter_map(|root| Path::new(root).canonicalize().ok()) + .any(|root| canonical.starts_with(root)); + + if allowed { + Ok(canonical) + } else { + Err(McpError::internal_error( + format!( + "File '{}' is outside configured allowed_file_paths.", + canonical.display() + ), + None, + )) } } } @@ -67,111 +285,188 @@ impl EmbeddedDebuggerToolHandler { // ============================================================================= #[tool(description = "List all available debug probes (J-Link, ST-Link, DAPLink, etc.)")] - async fn list_probes(&self, Parameters(_args): Parameters) -> Result { + async fn list_probes( + &self, + Parameters(_args): Parameters, + ) -> Result { debug!("Listing available debug probes"); - + // Real probe-rs integration let probes = Lister::new().list_all(); let message = if probes.is_empty() { "No debug probes found.\n\nPlease ensure your probe is connected and drivers are installed.\nSupported probes: J-Link, ST-Link, DAPLink, Black Magic Probe".to_string() } else { let mut result = format!("Found {} debug probe(s):\n\n", probes.len()); - + for (i, probe) in probes.iter().enumerate() { result.push_str(&format!("{}. {}\n", i + 1, probe.identifier)); - result.push_str(&format!(" VID:PID = {:04X}:{:04X}\n", probe.vendor_id, probe.product_id)); - + result.push_str(&format!( + " VID:PID = {:04X}:{:04X}\n", + probe.vendor_id, probe.product_id + )); + if let Some(serial) = &probe.serial_number { result.push_str(&format!(" Serial: {}\n", serial)); } - + result.push_str(&format!(" Probe Type: {:?}\n", probe.probe_type())); result.push('\n'); } - + result }; - + info!("Listed {} debug probes", probes.len()); Ok(CallToolResult::success(vec![Content::text(message)])) } #[tool(description = "Connect to a debug probe and target chip")] - async fn connect(&self, Parameters(args): Parameters) -> Result { - debug!("Connecting to probe '{}' and target '{}'", args.probe_selector, args.target_chip); - - // Check session limit - { - let sessions = self.sessions.read().await; - if sessions.len() >= self.max_sessions { - let error_msg = format!("Session limit exceeded. Maximum {} sessions allowed.", self.max_sessions); - return Err(McpError::internal_error(error_msg, None)); - } - } - + async fn connect( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Connecting to probe '{}' and target '{}'", + args.probe_selector, args.target_chip + ); + + let session_slot = self + .session_slots + .clone() + .try_acquire_owned() + .map_err(|_| { + McpError::internal_error( + format!( + "Session limit exceeded. Maximum {} sessions allowed.", + self.max_sessions + ), + None, + ) + })?; + // Real probe-rs implementation let probes = Lister::new().list_all(); - + if probes.is_empty() { return Err(McpError::internal_error( - "❌ No debug probes found\n\nPlease connect a supported probe (J-Link, ST-Link, DAPLink, etc.)".to_string(), + "No debug probes found. Please connect a supported probe (J-Link, ST-Link, DAPLink, etc.)".to_string(), None )); } - + let selected_probe = if args.probe_selector.to_lowercase() == "auto" { probes.first() } else { - probes.iter().find(|p| p.identifier.contains(&args.probe_selector)) + probes + .iter() + .find(|p| p.identifier.contains(&args.probe_selector)) }; match selected_probe { Some(probe_info) => { info!("Opening probe: {}", probe_info.identifier); match probe_info.open() { - Ok(probe) => { + Ok(mut probe) => { + let actual_speed = probe.set_speed(args.speed_khz).map_err(|e| { + McpError::internal_error( + format!( + "Failed to set probe speed to {} kHz: {}", + args.speed_khz, e + ), + None, + ) + })?; + + let permissions = if self.flash_erase_allowed() { + Permissions::new().allow_erase_all() + } else { + Permissions::new() + }; + + let connect_under_reset = + args.connect_under_reset || self.config.debugger.connect_under_reset; + let halt_after_connect = + args.halt_after_connect || self.config.debugger.halt_on_connect; + info!("Attaching to target: {}", args.target_chip); - match probe.attach(&args.target_chip, Permissions::default()) { - Ok(session) => { - let session_id = format!("session_{}", chrono::Utc::now().timestamp_millis()); - + let attach_result = if connect_under_reset { + probe.attach_under_reset(&args.target_chip, permissions) + } else { + probe.attach(&args.target_chip, permissions) + }; + + match attach_result { + Ok(mut session) => { + if halt_after_connect { + let mut core = session.core(0).map_err(|e| { + McpError::internal_error( + format!( + "Connected but failed to get core for halt: {}", + e + ), + None, + ) + })?; + core.halt(std::time::Duration::from_millis( + self.config.debugger.connection_timeout_ms, + )) + .map_err(|e| { + McpError::internal_error( + format!("Connected but failed to halt target: {}", e), + None, + ) + })?; + } + + let session_id = format!("session_{}", uuid::Uuid::new_v4()); + let debug_session = DebugSession { session_id: session_id.clone(), probe_identifier: probe_info.identifier.clone(), target_chip: args.target_chip.clone(), created_at: chrono::Utc::now(), session: Arc::new(tokio::sync::Mutex::new(session)), - rtt_manager: Arc::new(tokio::sync::Mutex::new(RttManager::new())), + rtt_manager: Arc::new(tokio::sync::Mutex::new( + RttManager::new(), + )), + _session_slot: session_slot, }; - + // Store session { let mut sessions = self.sessions.write().await; sessions.insert(session_id.clone(), Arc::new(debug_session)); } - + let message = format!( - "✅ Debug session established!\n\n\ + "Debug session established.\n\n\ Session ID: {}\n\ Probe: {} (VID:PID = {:04X}:{:04X})\n\ Target: {}\n\ + Speed: {} kHz\n\ + Connect under reset: {}\n\ + Halted after connect: {}\n\ Connected at: {}\n\n\ Target connection established and ready for debugging.\n\ Use this session ID for all debug operations.", session_id, probe_info.identifier, - probe_info.vendor_id, probe_info.product_id, + probe_info.vendor_id, + probe_info.product_id, args.target_chip, + actual_speed, + connect_under_reset, + halt_after_connect, chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC") ); - + info!("Created debug session: {}", session_id); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { error!("Failed to attach to target '{}': {}", args.target_chip, e); let error_msg = format!( - "❌ Failed to attach to target '{}'\n\n\ + "Failed to attach to target '{}'\n\n\ Error: {}\n\n\ Suggestions:\n\ - Check target chip name (try: STM32F407VGTx, nRF52840_xxAA)\n\ @@ -186,7 +481,7 @@ impl EmbeddedDebuggerToolHandler { Err(e) => { error!("Failed to open probe '{}': {}", probe_info.identifier, e); let error_msg = format!( - "❌ Failed to open probe '{}'\n\nError: {}\n\n\ + "Failed to open probe '{}'\n\nError: {}\n\n\ Suggestions:\n\ - Check probe drivers installation\n\ - Verify USB connection\n\ @@ -202,9 +497,9 @@ impl EmbeddedDebuggerToolHandler { .iter() .map(|p| format!("- {}", p.identifier)) .collect(); - + let error_msg = format!( - "❌ Probe '{}' not found\n\n\ + "Probe '{}' not found\n\n\ Available probes:\n{}\n\n\ Use 'auto' to connect to first available probe.", args.probe_selector, @@ -216,19 +511,22 @@ impl EmbeddedDebuggerToolHandler { } #[tool(description = "Disconnect from a debug session")] - async fn disconnect(&self, Parameters(args): Parameters) -> Result { + async fn disconnect( + &self, + Parameters(args): Parameters, + ) -> Result { debug!("Disconnecting session: {}", args.session_id); - + // Remove session from storage let removed_session = { let mut sessions = self.sessions.write().await; sessions.remove(&args.session_id) }; - + match removed_session { Some(session) => { let message = format!( - "✅ Debug session disconnected successfully\n\n\ + "Debug session disconnected successfully\n\n\ Session ID: {}\n\ Probe: {}\n\ Target: {}\n\ @@ -239,38 +537,36 @@ impl EmbeddedDebuggerToolHandler { session.target_chip, (chrono::Utc::now() - session.created_at).num_seconds() as f64 / 60.0 ); - + info!("Disconnected debug session: {}", args.session_id); Ok(CallToolResult::success(vec![Content::text(message)])) } None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'list_sessions' to see active sessions", args.session_id); + let error_msg = format!( + "Session '{}' not found\n\nUse 'connect' to establish a debug session first", + args.session_id + ); Err(McpError::internal_error(error_msg, None)) } } } #[tool(description = "Get basic information about a debug session")] - async fn probe_info(&self, Parameters(args): Parameters) -> Result { + async fn probe_info( + &self, + Parameters(args): Parameters, + ) -> Result { debug!("Getting probe info for session: {}", args.session_id); - + // Get session from storage - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; - + let session_arc = self.get_session(&args.session_id).await?; + // Calculate session duration - let duration_minutes = (chrono::Utc::now() - session_arc.created_at).num_seconds() as f64 / 60.0; - + let duration_minutes = + (chrono::Utc::now() - session_arc.created_at).num_seconds() as f64 / 60.0; + let message = format!( - "📊 Debug Session Information\n\n\ + "Debug Session Information\n\n\ Probe Information:\n\ - Identifier: {}\n\ - Connected: true\n\n\ @@ -287,7 +583,7 @@ impl EmbeddedDebuggerToolHandler { session_arc.created_at.format("%Y-%m-%d %H:%M:%S UTC"), duration_minutes ); - + info!("Retrieved probe info for session: {}", args.session_id); Ok(CallToolResult::success(vec![Content::text(message)])) } @@ -297,20 +593,14 @@ impl EmbeddedDebuggerToolHandler { // ============================================================================= #[tool(description = "Halt the target CPU execution")] - async fn halt(&self, Parameters(args): Parameters) -> Result { + async fn halt( + &self, + Parameters(args): Parameters, + ) -> Result { debug!("Halting target for session: {}", args.session_id); - - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; - + + let session_arc = self.get_session(&args.session_id).await?; + // Halt the target { let mut session = session_arc.session.lock().await; @@ -318,34 +608,43 @@ impl EmbeddedDebuggerToolHandler { Ok(core) => core, Err(e) => { error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error(format!("Failed to get core: {}", e), None)); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); } }; - + match core.halt(std::time::Duration::from_millis(1000)) { Ok(_) => { // Get status after halt match core.status() { Ok(_status) => { - let pc = core.read_core_reg(core.program_counter()).map(|v: RegisterValue| v.try_into().unwrap_or(0u32)).unwrap_or(0); - let sp = core.read_core_reg(core.stack_pointer()).map(|v: RegisterValue| v.try_into().unwrap_or(0u32)).unwrap_or(0); - + let pc = core + .read_core_reg(core.program_counter()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + let sp = core + .read_core_reg(core.stack_pointer()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + let message = format!( - "✅ Target halted successfully!\n\n\ + "Target halted successfully!\n\n\ Session ID: {}\n\ PC: 0x{:08X}\n\ SP: 0x{:08X}\n\ State: Halted\n", args.session_id, pc, sp ); - + info!("Halt completed for session: {}", args.session_id); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { warn!("Failed to get status after halt: {}", e); let message = format!( - "✅ Target halted successfully!\n\n\ + "Target halted successfully!\n\n\ Session ID: {}\n\ State: Halted\n", args.session_id @@ -355,8 +654,14 @@ impl EmbeddedDebuggerToolHandler { } } Err(e) => { - error!("Failed to halt target for session {}: {}", args.session_id, e); - Err(McpError::internal_error(format!("Failed to halt target: {}", e), None)) + error!( + "Failed to halt target for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to halt target: {}", e), + None, + )) } } } @@ -365,18 +670,9 @@ impl EmbeddedDebuggerToolHandler { #[tool(description = "Resume target CPU execution")] async fn run(&self, Parameters(args): Parameters) -> Result { debug!("Running target for session: {}", args.session_id); - - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; - + + let session_arc = self.get_session(&args.session_id).await?; + // Resume the target { let mut session = session_arc.session.lock().await; @@ -384,46 +680,59 @@ impl EmbeddedDebuggerToolHandler { Ok(core) => core, Err(e) => { error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error(format!("Failed to get core: {}", e), None)); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); } }; - + match core.run() { Ok(_) => { let message = format!( - "✅ Target resumed execution successfully!\n\n\ + "Target resumed execution successfully!\n\n\ Session ID: {}\n\ Status: Running\n\n\ The target is now executing code. Use 'halt' to stop execution.", args.session_id ); - + info!("Run completed for session: {}", args.session_id); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { - error!("Failed to run target for session {}: {}", args.session_id, e); - Err(McpError::internal_error(format!("Failed to run target: {}", e), None)) + error!( + "Failed to run target for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to run target: {}", e), + None, + )) } } } } #[tool(description = "Reset the target CPU")] - async fn reset(&self, Parameters(args): Parameters) -> Result { + async fn reset( + &self, + Parameters(args): Parameters, + ) -> Result { debug!("Resetting target for session: {}", args.session_id); - - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; - + + if args.reset_type != "hardware" { + return Err(McpError::internal_error( + format!( + "Unsupported reset_type '{}'. probe-rs core reset is exposed as 'hardware' by this server.", + args.reset_type + ), + None, + )); + } + + let session_arc = self.get_session(&args.session_id).await?; + // Reset the target { let mut session = session_arc.session.lock().await; @@ -431,24 +740,35 @@ impl EmbeddedDebuggerToolHandler { Ok(core) => core, Err(e) => { error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error(format!("Failed to get core: {}", e), None)); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); } }; - - match core.reset() { + + let reset_result = if args.halt_after_reset { + core.reset_and_halt(std::time::Duration::from_millis( + self.config.debugger.connection_timeout_ms, + )) + .map(|_| ()) + } else { + core.reset() + }; + + match reset_result { Ok(_) => { - if args.halt_after_reset { - match core.halt(std::time::Duration::from_millis(1000)) { - Ok(_) => {}, - Err(e) => warn!("Failed to halt after reset: {}", e), - } - } - - let pc = core.read_core_reg(core.program_counter()).map(|v: RegisterValue| v.try_into().unwrap_or(0u32)).unwrap_or(0); - let sp = core.read_core_reg(core.stack_pointer()).map(|v: RegisterValue| v.try_into().unwrap_or(0u32)).unwrap_or(0); - + let pc = core + .read_core_reg(core.program_counter()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + let sp = core + .read_core_reg(core.stack_pointer()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + let message = format!( - "✅ Target reset completed successfully!\n\n\ + "Target reset completed successfully.\n\n\ Session ID: {}\n\ Reset type: {}\n\ Halted after reset: {}\n\ @@ -458,36 +778,41 @@ impl EmbeddedDebuggerToolHandler { args.session_id, args.reset_type, args.halt_after_reset, - pc, sp, - if args.halt_after_reset { "Halted" } else { "Running" } + pc, + sp, + if args.halt_after_reset { + "Halted" + } else { + "Running" + } ); - + info!("Reset completed for session: {}", args.session_id); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { - error!("Failed to reset target for session {}: {}", args.session_id, e); - Err(McpError::internal_error(format!("Failed to reset target: {}", e), None)) + error!( + "Failed to reset target for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to reset target: {}", e), + None, + )) } } } } #[tool(description = "Execute a single instruction step")] - async fn step(&self, Parameters(args): Parameters) -> Result { + async fn step( + &self, + Parameters(args): Parameters, + ) -> Result { debug!("Single stepping target for session: {}", args.session_id); - - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; - + + let session_arc = self.get_session(&args.session_id).await?; + // Single step the target { let mut session = session_arc.session.lock().await; @@ -495,50 +820,59 @@ impl EmbeddedDebuggerToolHandler { Ok(core) => core, Err(e) => { error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error(format!("Failed to get core: {}", e), None)); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); } }; - + match core.step() { Ok(_) => { - let pc = core.read_core_reg(core.program_counter()).map(|v: RegisterValue| v.try_into().unwrap_or(0u32)).unwrap_or(0); - let sp = core.read_core_reg(core.stack_pointer()).map(|v: RegisterValue| v.try_into().unwrap_or(0u32)).unwrap_or(0); - + let pc = core + .read_core_reg(core.program_counter()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + let sp = core + .read_core_reg(core.stack_pointer()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + let message = format!( - "✅ Single step completed successfully!\n\n\ + "Single step completed successfully!\n\n\ Session ID: {}\n\ PC: 0x{:08X}\n\ SP: 0x{:08X}\n\ State: Halted\n", args.session_id, pc, sp ); - + info!("Step completed for session: {}", args.session_id); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { - error!("Failed to step target for session {}: {}", args.session_id, e); - Err(McpError::internal_error(format!("Failed to step target: {}", e), None)) + error!( + "Failed to step target for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to step target: {}", e), + None, + )) } } } } #[tool(description = "Get current status of the target CPU and debug session")] - async fn get_status(&self, Parameters(args): Parameters) -> Result { + async fn get_status( + &self, + Parameters(args): Parameters, + ) -> Result { debug!("Getting status for session: {}", args.session_id); - - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; - + + let session_arc = self.get_session(&args.session_id).await?; + // Get target status { let mut session = session_arc.session.lock().await; @@ -546,24 +880,33 @@ impl EmbeddedDebuggerToolHandler { Ok(core) => core, Err(e) => { error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error(format!("Failed to get core: {}", e), None)); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); } }; - + match core.status() { Ok(status) => { - let pc = core.read_core_reg(core.program_counter()).map(|v: RegisterValue| v.try_into().unwrap_or(0u32)).unwrap_or(0); - let sp = core.read_core_reg(core.stack_pointer()).map(|v: RegisterValue| v.try_into().unwrap_or(0u32)).unwrap_or(0); - + let pc = core + .read_core_reg(core.program_counter()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + let sp = core + .read_core_reg(core.stack_pointer()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + let is_halted = matches!(status, CoreStatus::Halted(_)); let halt_reason = match status { CoreStatus::Halted(reason) => format!("{:?}", reason), CoreStatus::Running => "N/A".to_string(), _ => "Unknown".to_string(), }; - + let message = format!( - "📊 Debug Session Status\n\n\ + "Debug Session Status\n\n\ Core Information:\n\ - PC: 0x{:08X}\n\ - SP: 0x{:08X}\n\ @@ -575,7 +918,8 @@ impl EmbeddedDebuggerToolHandler { - Target: {}\n\ - Probe: {}\n\ - Duration: {:.1} minutes\n", - pc, sp, + pc, + sp, if is_halted { "Halted" } else { "Running" }, halt_reason, args.session_id, @@ -583,12 +927,18 @@ impl EmbeddedDebuggerToolHandler { session_arc.probe_identifier, (chrono::Utc::now() - session_arc.created_at).num_seconds() as f64 / 60.0 ); - + Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { - error!("Failed to get core status for session {}: {}", args.session_id, e); - Err(McpError::internal_error(format!("Failed to get core status: {}", e), None)) + error!( + "Failed to get core status for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to get core status: {}", e), + None, + )) } } } @@ -599,28 +949,29 @@ impl EmbeddedDebuggerToolHandler { // ============================================================================= #[tool(description = "Read memory from the target")] - async fn read_memory(&self, Parameters(args): Parameters) -> Result { - debug!("Reading memory for session: {} at address {}", args.session_id, args.address); - + async fn read_memory( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Reading memory for session: {} at address {}", + args.session_id, args.address + ); + // Parse address let address = match parse_address(&args.address) { Ok(addr) => addr, Err(e) => { error!("Invalid address '{}': {}", args.address, e); - return Err(McpError::internal_error(format!("Invalid address '{}': {}", args.address, e), None)); + return Err(McpError::internal_error( + format!("Invalid address '{}': {}", args.address, e), + None, + )); } }; - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; + let session_arc = self.get_session(&args.session_id).await?; + self.ensure_memory_read_allowed(&session_arc, address, args.size)?; // Read memory { @@ -629,18 +980,21 @@ impl EmbeddedDebuggerToolHandler { Ok(core) => core, Err(e) => { error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error(format!("Failed to get core: {}", e), None)); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); } }; - - let mut data = vec![0u8; args.size as usize]; + + let mut data = vec![0u8; args.size]; match core.read(address, &mut data) { Ok(_) => { debug!("Read {} bytes from address 0x{:08X}", data.len(), address); - + let formatted_data = format_memory_data(&data, &args.format, address); let message = format!( - "📖 Memory read completed successfully!\n\n\ + "Memory read completed successfully.\n\n\ Session ID: {}\n\ Address: 0x{:08X}\n\ Size: {} bytes\n\ @@ -648,28 +1002,43 @@ impl EmbeddedDebuggerToolHandler { Data:\n{}", args.session_id, address, args.size, args.format, formatted_data ); - + info!("Memory read completed for session: {}", args.session_id); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { - error!("Failed to read memory for session {}: {}", args.session_id, e); - Err(McpError::internal_error(format!("Failed to read memory: {}", e), None)) + error!( + "Failed to read memory for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to read memory: {}", e), + None, + )) } } } } #[tool(description = "Write memory to the target")] - async fn write_memory(&self, Parameters(args): Parameters) -> Result { - debug!("Writing memory for session: {} at address {}", args.session_id, args.address); - + async fn write_memory( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Writing memory for session: {} at address {}", + args.session_id, args.address + ); + // Parse address let address = match parse_address(&args.address) { Ok(addr) => addr, Err(e) => { error!("Invalid address '{}': {}", args.address, e); - return Err(McpError::internal_error(format!("Invalid address '{}': {}", args.address, e), None)); + return Err(McpError::internal_error( + format!("Invalid address '{}': {}", args.address, e), + None, + )); } }; @@ -678,20 +1047,15 @@ impl EmbeddedDebuggerToolHandler { Ok(data) => data, Err(e) => { error!("Invalid data '{}': {}", args.data, e); - return Err(McpError::internal_error(format!("Invalid data '{}': {}", args.data, e), None)); + return Err(McpError::internal_error( + format!("Invalid data '{}': {}", args.data, e), + None, + )); } }; - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; + let session_arc = self.get_session(&args.session_id).await?; + self.ensure_memory_write_allowed(&session_arc, address, data.len())?; // Write memory { @@ -700,28 +1064,41 @@ impl EmbeddedDebuggerToolHandler { Ok(core) => core, Err(e) => { error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error(format!("Failed to get core: {}", e), None)); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); } }; - + match core.write(address, &data) { Ok(_) => { let message = format!( - "✏️ Memory write completed successfully!\n\n\ + "Memory write completed successfully.\n\n\ Session ID: {}\n\ Address: 0x{:08X}\n\ Data: {}\n\ Format: {}\n\ Bytes written: {}", - args.session_id, address, args.data, args.format, data.len() + args.session_id, + address, + args.data, + args.format, + data.len() ); - + info!("Memory write completed for session: {}", args.session_id); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { - error!("Failed to write memory for session {}: {}", args.session_id, e); - Err(McpError::internal_error(format!("Failed to write memory: {}", e), None)) + error!( + "Failed to write memory for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to write memory: {}", e), + None, + )) } } } @@ -732,28 +1109,38 @@ impl EmbeddedDebuggerToolHandler { // ============================================================================= #[tool(description = "Set a breakpoint at the specified address")] - async fn set_breakpoint(&self, Parameters(args): Parameters) -> Result { - debug!("Setting breakpoint for session: {} at address {}", args.session_id, args.address); - + async fn set_breakpoint( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Setting breakpoint for session: {} at address {}", + args.session_id, args.address + ); + + if args.breakpoint_type != "hardware" { + return Err(McpError::internal_error( + format!( + "Unsupported breakpoint_type '{}'. Only hardware breakpoints are implemented.", + args.breakpoint_type + ), + None, + )); + } + // Parse address let address = match parse_address(&args.address) { Ok(addr) => addr, Err(e) => { error!("Invalid address '{}': {}", args.address, e); - return Err(McpError::internal_error(format!("Invalid address '{}': {}", args.address, e), None)); + return Err(McpError::internal_error( + format!("Invalid address '{}': {}", args.address, e), + None, + )); } }; - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; + let session_arc = self.get_session(&args.session_id).await?; // Set breakpoint { @@ -762,42 +1149,63 @@ impl EmbeddedDebuggerToolHandler { Ok(core) => core, Err(e) => { error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error(format!("Failed to get core: {}", e), None)); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); } }; - + match core.set_hw_breakpoint(address) { Ok(_) => { let message = format!( - "🎯 Breakpoint set successfully!\n\n\ + "Breakpoint set successfully.\n\n\ Session ID: {}\n\ Address: 0x{:08X}\n\ Type: Hardware breakpoint\n\n\ The target will halt when execution reaches this address.", args.session_id, address ); - - info!("Breakpoint set for session: {} at 0x{:08X}", args.session_id, address); + + info!( + "Breakpoint set for session: {} at 0x{:08X}", + args.session_id, address + ); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { - error!("Failed to set breakpoint for session {}: {}", args.session_id, e); - Err(McpError::internal_error(format!("Failed to set breakpoint: {}", e), None)) + error!( + "Failed to set breakpoint for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to set breakpoint: {}", e), + None, + )) } } } } #[tool(description = "Clear a breakpoint at the specified address")] - async fn clear_breakpoint(&self, Parameters(args): Parameters) -> Result { - debug!("Clearing breakpoint for session: {} at address {}", args.session_id, args.address); - + async fn clear_breakpoint( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Clearing breakpoint for session: {} at address {}", + args.session_id, args.address + ); + // Parse address let address = match parse_address(&args.address) { Ok(addr) => addr, Err(e) => { error!("Invalid address '{}': {}", args.address, e); - return Err(McpError::internal_error(format!("Invalid address '{}': {}", args.address, e), None)); + return Err(McpError::internal_error( + format!("Invalid address '{}': {}", args.address, e), + None, + )); } }; @@ -806,7 +1214,7 @@ impl EmbeddedDebuggerToolHandler { match sessions.get(&args.session_id) { Some(session) => session.clone(), None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); + let error_msg = format!("Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); return Err(McpError::internal_error(error_msg, None)); } } @@ -819,26 +1227,38 @@ impl EmbeddedDebuggerToolHandler { Ok(core) => core, Err(e) => { error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error(format!("Failed to get core: {}", e), None)); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); } }; - + match core.clear_hw_breakpoint(address) { Ok(_) => { let message = format!( - "🎯 Breakpoint cleared successfully!\n\n\ + "Breakpoint cleared successfully.\n\n\ Session ID: {}\n\ Address: 0x{:08X}\n\n\ The breakpoint has been removed.", args.session_id, address ); - - info!("Breakpoint cleared for session: {} at 0x{:08X}", args.session_id, address); + + info!( + "Breakpoint cleared for session: {} at 0x{:08X}", + args.session_id, address + ); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { - error!("Failed to clear breakpoint for session {}: {}", args.session_id, e); - Err(McpError::internal_error(format!("Failed to clear breakpoint: {}", e), None)) + error!( + "Failed to clear breakpoint for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to clear breakpoint: {}", e), + None, + )) } } } @@ -849,27 +1269,20 @@ impl EmbeddedDebuggerToolHandler { // ============================================================================= #[tool(description = "Attach to RTT (Real-Time Transfer) for communication with target")] - async fn rtt_attach(&self, Parameters(args): Parameters) -> Result { + async fn rtt_attach( + &self, + Parameters(args): Parameters, + ) -> Result { debug!("Attaching RTT for session: {}", args.session_id); - - // Get session from storage - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; + + let session_arc = self.get_session(&args.session_id).await?; // Parse control block address if provided let control_block_address = if let Some(addr_str) = args.control_block_address { match parse_address(&addr_str) { Ok(addr) => Some(addr), Err(e) => { - let error_msg = format!("❌ Invalid control block address '{}': {}", addr_str, e); + let error_msg = format!("Invalid control block address '{}': {}", addr_str, e); return Err(McpError::internal_error(error_msg, None)); } } @@ -882,10 +1295,16 @@ impl EmbeddedDebuggerToolHandler { let mut parsed_ranges = Vec::new(); for range in ranges { let start = parse_address(&range.start).map_err(|e| { - McpError::internal_error(format!("Invalid start address '{}': {}", range.start, e), None) + McpError::internal_error( + format!("Invalid start address '{}': {}", range.start, e), + None, + ) })?; let end = parse_address(&range.end).map_err(|e| { - McpError::internal_error(format!("Invalid end address '{}': {}", range.end, e), None) + McpError::internal_error( + format!("Invalid end address '{}': {}", range.end, e), + None, + ) })?; parsed_ranges.push((start, end)); } @@ -897,28 +1316,38 @@ impl EmbeddedDebuggerToolHandler { // Attach RTT { let mut rtt_manager = session_arc.rtt_manager.lock().await; - match rtt_manager.attach(session_arc.session.clone(), control_block_address, memory_ranges).await { + match rtt_manager + .attach( + session_arc.session.clone(), + control_block_address, + memory_ranges, + ) + .await + { Ok(_) => { let up_channels = rtt_manager.up_channel_count(); let down_channels = rtt_manager.down_channel_count(); - + let message = format!( - "✅ RTT attached successfully!\n\n\ + "RTT attached successfully!\n\n\ Session ID: {}\n\ - Up Channels (Target→Host): {}\n\ - Down Channels (Host→Target): {}\n\n\ + Up Channels (Target to Host): {}\n\ + Down Channels (Host to Target): {}\n\n\ RTT is now ready for real-time communication with the target.\n\ Use 'rtt_read' to read from target and 'rtt_write' to send data to target.", args.session_id, up_channels, down_channels ); - + info!("RTT attached successfully for session: {}", args.session_id); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { - error!("Failed to attach RTT for session {}: {}", args.session_id, e); + error!( + "Failed to attach RTT for session {}: {}", + args.session_id, e + ); let error_msg = format!( - "❌ Failed to attach RTT\n\n\ + "Failed to attach RTT\n\n\ Session ID: {}\n\ Error: {}\n\n\ Suggestions:\n\ @@ -935,20 +1364,13 @@ impl EmbeddedDebuggerToolHandler { } #[tool(description = "Detach from RTT communication")] - async fn rtt_detach(&self, Parameters(args): Parameters) -> Result { + async fn rtt_detach( + &self, + Parameters(args): Parameters, + ) -> Result { debug!("Detaching RTT for session: {}", args.session_id); - - // Get session from storage - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; + + let session_arc = self.get_session(&args.session_id).await?; // Detach RTT { @@ -956,18 +1378,21 @@ impl EmbeddedDebuggerToolHandler { match rtt_manager.detach().await { Ok(_) => { let message = format!( - "✅ RTT detached successfully\n\n\ + "RTT detached successfully\n\n\ Session ID: {}\n\n\ RTT communication has been closed.", args.session_id ); - + info!("RTT detached successfully for session: {}", args.session_id); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { - error!("Failed to detach RTT for session {}: {}", args.session_id, e); - let error_msg = format!("❌ Failed to detach RTT: {}", e); + error!( + "Failed to detach RTT for session {}: {}", + args.session_id, e + ); + let error_msg = format!("Failed to detach RTT: {}", e); Err(McpError::internal_error(error_msg, None)) } } @@ -975,30 +1400,47 @@ impl EmbeddedDebuggerToolHandler { } #[tool(description = "Read data from RTT up channel (target to host)")] - async fn rtt_read(&self, Parameters(args): Parameters) -> Result { - debug!("Reading from RTT channel {} for session: {}", args.channel, args.session_id); - - // Get session from storage - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; + async fn rtt_read( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Reading from RTT channel {} for session: {}", + args.channel, args.session_id + ); + + let session_arc = self.get_session(&args.session_id).await?; + if args.max_bytes == 0 { + return Err(McpError::internal_error( + "max_bytes must be greater than zero.".to_string(), + None, + )); + } + if args.max_bytes > self.config.rtt.buffer_size { + return Err(McpError::internal_error( + format!( + "max_bytes {} exceeds configured RTT buffer size {}.", + args.max_bytes, self.config.rtt.buffer_size + ), + None, + )); + } // Read from RTT { let mut rtt_manager = session_arc.rtt_manager.lock().await; if !rtt_manager.is_attached() { - let error_msg = format!("❌ RTT not attached for session '{}'\n\nUse 'rtt_attach' first", args.session_id); + let error_msg = format!( + "RTT not attached for session '{}'\n\nUse 'rtt_attach' first", + args.session_id + ); return Err(McpError::internal_error(error_msg, None)); } - match rtt_manager.read_channel(args.channel).await { + match rtt_manager + .read_channel(args.channel, args.max_bytes, args.timeout_ms) + .await + { Ok(data) => { let data_len = data.len(); let data_str = if data.is_empty() { @@ -1007,31 +1449,40 @@ impl EmbeddedDebuggerToolHandler { // Try to decode as UTF-8, fall back to hex if not valid match String::from_utf8(data.clone()) { Ok(text) => { - if text.chars().all(|c| c.is_ascii_graphic() || c.is_ascii_whitespace()) { + if text + .chars() + .all(|c| c.is_ascii_graphic() || c.is_ascii_whitespace()) + { format!("Text: {}", text) } else { format!("Mixed: {} (hex: {})", text, hex::encode(&data)) } } - Err(_) => format!("Binary data (hex): {}", hex::encode(&data)) + Err(_) => format!("Binary data (hex): {}", hex::encode(&data)), } }; let message = format!( - "📥 RTT Read from Channel {}\n\n\ + "RTT Read from Channel {}\n\n\ Session ID: {}\n\ Bytes Read: {}\n\n\ Data:\n{}", args.channel, args.session_id, data_len, data_str ); - - debug!("Read {} bytes from RTT channel {} for session: {}", data_len, args.channel, args.session_id); + + debug!( + "Read {} bytes from RTT channel {} for session: {}", + data_len, args.channel, args.session_id + ); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { - error!("Failed to read from RTT channel {} for session {}: {}", args.channel, args.session_id, e); + error!( + "Failed to read from RTT channel {} for session {}: {}", + args.channel, args.session_id, e + ); let error_msg = format!( - "❌ Failed to read from RTT channel {}\n\n\ + "Failed to read from RTT channel {}\n\n\ Session ID: {}\n\ Error: {}", args.channel, args.session_id, e @@ -1043,16 +1494,22 @@ impl EmbeddedDebuggerToolHandler { } #[tool(description = "Write data to RTT down channel (host to target)")] - async fn rtt_write(&self, Parameters(args): Parameters) -> Result { - debug!("Writing to RTT channel {} for session: {}", args.channel, args.session_id); - + async fn rtt_write( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Writing to RTT channel {} for session: {}", + args.channel, args.session_id + ); + // Get session from storage let session_arc = { let sessions = self.sessions.read().await; match sessions.get(&args.session_id) { Some(session) => session.clone(), None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); + let error_msg = format!("Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); return Err(McpError::internal_error(error_msg, None)); } } @@ -1061,30 +1518,29 @@ impl EmbeddedDebuggerToolHandler { // Parse data based on encoding let data_bytes = match args.encoding.as_str() { "utf8" => args.data.as_bytes().to_vec(), - "hex" => { - match hex::decode(&args.data) { - Ok(bytes) => bytes, - Err(e) => { - let error_msg = format!("❌ Invalid hex data '{}': {}", args.data, e); - return Err(McpError::internal_error(error_msg, None)); - } + "hex" => match hex::decode(&args.data) { + Ok(bytes) => bytes, + Err(e) => { + let error_msg = format!("Invalid hex data '{}': {}", args.data, e); + return Err(McpError::internal_error(error_msg, None)); } - } + }, "binary" => { // Parse binary string like "10110011 11001100" let binary_str = args.data.replace(' ', ""); if binary_str.len() % 8 != 0 { - let error_msg = format!("❌ Binary data must be multiple of 8 bits: '{}'", args.data); + let error_msg = + format!("Binary data must be multiple of 8 bits: '{}'", args.data); return Err(McpError::internal_error(error_msg, None)); } - + let mut bytes = Vec::new(); for chunk in binary_str.chars().collect::>().chunks(8) { let byte_str: String = chunk.iter().collect(); match u8::from_str_radix(&byte_str, 2) { Ok(byte) => bytes.push(byte), Err(e) => { - let error_msg = format!("❌ Invalid binary byte '{}': {}", byte_str, e); + let error_msg = format!("Invalid binary byte '{}': {}", byte_str, e); return Err(McpError::internal_error(error_msg, None)); } } @@ -1092,7 +1548,10 @@ impl EmbeddedDebuggerToolHandler { bytes } _ => { - let error_msg = format!("❌ Unsupported encoding '{}'. Use 'utf8', 'hex', or 'binary'", args.encoding); + let error_msg = format!( + "Unsupported encoding '{}'. Use 'utf8', 'hex', or 'binary'", + args.encoding + ); return Err(McpError::internal_error(error_msg, None)); } }; @@ -1101,14 +1560,17 @@ impl EmbeddedDebuggerToolHandler { { let mut rtt_manager = session_arc.rtt_manager.lock().await; if !rtt_manager.is_attached() { - let error_msg = format!("❌ RTT not attached for session '{}'\n\nUse 'rtt_attach' first", args.session_id); + let error_msg = format!( + "RTT not attached for session '{}'\n\nUse 'rtt_attach' first", + args.session_id + ); return Err(McpError::internal_error(error_msg, None)); } match rtt_manager.write_channel(args.channel, &data_bytes).await { Ok(bytes_written) => { let message = format!( - "📤 RTT Write to Channel {}\n\n\ + "RTT Write to Channel {}\n\n\ Session ID: {}\n\ Data: {}\n\ Encoding: {}\n\ @@ -1116,14 +1578,20 @@ impl EmbeddedDebuggerToolHandler { Data sent successfully to target.", args.channel, args.session_id, args.data, args.encoding, bytes_written ); - - info!("Wrote {} bytes to RTT channel {} for session: {}", bytes_written, args.channel, args.session_id); + + info!( + "Wrote {} bytes to RTT channel {} for session: {}", + bytes_written, args.channel, args.session_id + ); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { - error!("Failed to write to RTT channel {} for session {}: {}", args.channel, args.session_id, e); + error!( + "Failed to write to RTT channel {} for session {}: {}", + args.channel, args.session_id, e + ); let error_msg = format!( - "❌ Failed to write to RTT channel {}\n\n\ + "Failed to write to RTT channel {}\n\n\ Session ID: {}\n\ Error: {}", args.channel, args.session_id, e @@ -1135,16 +1603,19 @@ impl EmbeddedDebuggerToolHandler { } #[tool(description = "List available RTT channels")] - async fn rtt_channels(&self, Parameters(args): Parameters) -> Result { + async fn rtt_channels( + &self, + Parameters(args): Parameters, + ) -> Result { debug!("Listing RTT channels for session: {}", args.session_id); - + // Get session from storage let session_arc = { let sessions = self.sessions.read().await; match sessions.get(&args.session_id) { Some(session) => session.clone(), None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); + let error_msg = format!("Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); return Err(McpError::internal_error(error_msg, None)); } } @@ -1154,13 +1625,16 @@ impl EmbeddedDebuggerToolHandler { { let rtt_manager = session_arc.rtt_manager.lock().await; if !rtt_manager.is_attached() { - let error_msg = format!("❌ RTT not attached for session '{}'\n\nUse 'rtt_attach' first", args.session_id); + let error_msg = format!( + "RTT not attached for session '{}'\n\nUse 'rtt_attach' first", + args.session_id + ); return Err(McpError::internal_error(error_msg, None)); } let channels = rtt_manager.get_channels(); let channel_count = channels.len(); - + if channels.is_empty() { let message = format!( "📋 RTT Channels\n\n\ @@ -1172,11 +1646,11 @@ impl EmbeddedDebuggerToolHandler { } let mut message = format!("📋 RTT Channels\n\nSession ID: {}\n\n", args.session_id); - + // Group channels by direction let mut up_channels = Vec::new(); let mut down_channels = Vec::new(); - + for channel in &channels { match channel.direction { crate::rtt::ChannelDirection::Up => up_channels.push(channel), @@ -1185,7 +1659,7 @@ impl EmbeddedDebuggerToolHandler { } if !up_channels.is_empty() { - message.push_str("📥 Up Channels (Target → Host):\n"); + message.push_str("Up Channels (Target to Host):\n"); for channel in up_channels { message.push_str(&format!( " {}. {} (Size: {} bytes, Mode: {})\n", @@ -1196,7 +1670,7 @@ impl EmbeddedDebuggerToolHandler { } if !down_channels.is_empty() { - message.push_str("📤 Down Channels (Host → Target):\n"); + message.push_str("Down Channels (Host to Target):\n"); for channel in down_channels { message.push_str(&format!( " {}. {} (Size: {} bytes, Mode: {})\n", @@ -1205,7 +1679,10 @@ impl EmbeddedDebuggerToolHandler { } } - info!("Listed {} RTT channels for session: {}", channel_count, args.session_id); + info!( + "Listed {} RTT channels for session: {}", + channel_count, args.session_id + ); Ok(CallToolResult::success(vec![Content::text(message)])) } } @@ -1215,35 +1692,50 @@ impl EmbeddedDebuggerToolHandler { // ============================================================================= #[tool(description = "Erase flash memory sectors or entire chip")] - async fn flash_erase(&self, Parameters(args): Parameters) -> Result { - debug!("Flash erase for session: {}, type: {}", args.session_id, args.erase_type); - - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; + async fn flash_erase( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Flash erase for session: {}, type: {}", + args.session_id, args.erase_type + ); + + self.ensure_flash_erase_allowed()?; + let session_arc = self.get_session(&args.session_id).await?; // Parse erase type and parameters let erase_type = match args.erase_type.as_str() { "all" => crate::flash::EraseType::All, "sectors" => { let address = match args.address { - Some(addr_str) => parse_address(&addr_str).map_err(|e| McpError::internal_error(e, None))?, - None => return Err(McpError::internal_error("Address required for sector erase".to_string(), None)), + Some(addr_str) => { + parse_address(&addr_str).map_err(|e| McpError::internal_error(e, None))? + } + None => { + return Err(McpError::internal_error( + "Address required for sector erase".to_string(), + None, + )) + } }; let size = match args.size { Some(sz) => sz as usize, - None => return Err(McpError::internal_error("Size required for sector erase".to_string(), None)), + None => { + return Err(McpError::internal_error( + "Size required for sector erase".to_string(), + None, + )) + } }; crate::flash::EraseType::Sectors { address, size } } - _ => return Err(McpError::internal_error(format!("Invalid erase type: {}", args.erase_type), None)), + _ => { + return Err(McpError::internal_error( + format!("Invalid erase type: {}", args.erase_type), + None, + )) + } }; // Perform erase operation @@ -1252,7 +1744,7 @@ impl EmbeddedDebuggerToolHandler { match crate::flash::FlashManager::erase_flash(&mut session, erase_type).await { Ok(result) => { let message = format!( - "✅ Flash erase completed successfully!\n\n\ + "Flash erase completed successfully.\n\n\ Session ID: {}\n\ Erase Type: {}\n\ Duration: {}ms\n\ @@ -1266,14 +1758,14 @@ impl EmbeddedDebuggerToolHandler { None => "Full chip erased".to_string(), } ); - + info!("Flash erase completed for session: {}", args.session_id); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { error!("Flash erase failed for session {}: {}", args.session_id, e); let error_msg = format!( - "❌ Flash erase failed\n\n\ + "Flash erase failed\n\n\ Session ID: {}\n\ Error: {}\n\n\ Suggestions:\n\ @@ -1289,28 +1781,31 @@ impl EmbeddedDebuggerToolHandler { } #[tool(description = "Program file to flash memory (supports ELF, HEX, BIN)")] - async fn flash_program(&self, Parameters(args): Parameters) -> Result { - debug!("Flash program for session: {}, file: {}", args.session_id, args.file_path); - - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; + async fn flash_program( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Flash program for session: {}, file: {}", + args.session_id, args.file_path + ); + + let session_arc = self.get_session(&args.session_id).await?; // Parse file path and format - let file_path = std::path::Path::new(&args.file_path); + let file_path = + self.resolve_allowed_file_path(&args.file_path, self.config.flash.max_binary_size)?; let format = match args.format.as_str() { "auto" => crate::flash::FileFormat::Auto, "elf" => crate::flash::FileFormat::Elf, "hex" => crate::flash::FileFormat::Hex, "bin" => crate::flash::FileFormat::Bin, - _ => return Err(McpError::internal_error(format!("Unsupported format: {}", args.format), None)), + _ => { + return Err(McpError::internal_error( + format!("Unsupported format: {}", args.format), + None, + )) + } }; // Parse base address if provided @@ -1323,10 +1818,19 @@ impl EmbeddedDebuggerToolHandler { // Perform programming operation { let mut session = session_arc.session.lock().await; - match crate::flash::FlashManager::program_file(&mut session, file_path, format, base_address).await { + let verify = args.verify || self.config.flash.verify_after_program; + match crate::flash::FlashManager::program_file( + &mut session, + &file_path, + format, + base_address, + verify, + ) + .await + { Ok(result) => { let message = format!( - "✅ Flash programming completed successfully!\n\n\ + "Flash programming completed successfully.\n\n\ Session ID: {}\n\ File: {}\n\ Format: {}\n\ @@ -1335,24 +1839,30 @@ impl EmbeddedDebuggerToolHandler { Verification: {}\n\n\ Firmware has been programmed to flash memory.", args.session_id, - args.file_path, + file_path.display(), args.format, result.bytes_programmed, result.programming_time_ms, match result.verification_result { - Some(true) => "✅ Passed", - Some(false) => "❌ Failed", + Some(true) => "Passed", + Some(false) => "Failed", None => "Not performed", } ); - - info!("Flash programming completed for session: {}", args.session_id); + + info!( + "Flash programming completed for session: {}", + args.session_id + ); Ok(CallToolResult::success(vec![Content::text(message)])) } Err(e) => { - error!("Flash programming failed for session {}: {}", args.session_id, e); + error!( + "Flash programming failed for session {}: {}", + args.session_id, e + ); let error_msg = format!( - "❌ Flash programming failed\n\n\ + "Flash programming failed\n\n\ Session ID: {}\n\ File: {}\n\ Error: {}\n\n\ @@ -1370,41 +1880,82 @@ impl EmbeddedDebuggerToolHandler { } #[tool(description = "Verify flash memory contents")] - async fn flash_verify(&self, Parameters(args): Parameters) -> Result { + async fn flash_verify( + &self, + Parameters(args): Parameters, + ) -> Result { debug!("Flash verify for session: {}", args.session_id); - + let session_arc = { let sessions = self.sessions.read().await; match sessions.get(&args.session_id) { Some(session) => session.clone(), None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); + let error_msg = format!("Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); return Err(McpError::internal_error(error_msg, None)); } } }; // Parse address - let address = parse_address(&args.address).map_err(|e| McpError::internal_error(e, None))?; + let address = + parse_address(&args.address).map_err(|e| McpError::internal_error(e, None))?; // Get expected data let expected_data = if let Some(file_path) = &args.file_path { - // Read from file - std::fs::read(file_path) - .map_err(|e| McpError::internal_error(format!("Failed to read file {}: {}", file_path, e), None))? + let file_path = + self.resolve_allowed_file_path(file_path, self.config.flash.max_binary_size)?; + let extension = file_path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_ascii_lowercase()); + if matches!(extension.as_deref(), Some("elf" | "hex")) { + return Err(McpError::internal_error( + format!( + "flash_verify compares raw bytes only; '{}' must be verified with a raw BIN file or hex data.", + file_path.display() + ), + None, + )); + } + std::fs::read(&file_path).map_err(|e| { + McpError::internal_error( + format!("Failed to read file {}: {}", file_path.display(), e), + None, + ) + })? } else if let Some(hex_data) = &args.data { // Parse hex data match parse_data(hex_data, "hex") { Ok(data) => data, - Err(e) => return Err(McpError::internal_error(format!("Invalid hex data: {}", e), None)), + Err(e) => { + return Err(McpError::internal_error( + format!("Invalid hex data: {}", e), + None, + )) + } } } else { - return Err(McpError::internal_error("Either file_path or data must be provided".to_string(), None)); + return Err(McpError::internal_error( + "Either file_path or data must be provided".to_string(), + None, + )); }; - // Limit to specified size - let expected_data = if expected_data.len() > args.size as usize { - &expected_data[..args.size as usize] + let verify_size = args.size as usize; + if expected_data.len() < verify_size { + return Err(McpError::internal_error( + format!( + "Expected data has {} bytes, fewer than requested verify size {}.", + expected_data.len(), + verify_size + ), + None, + )); + } + + let expected_data = if expected_data.len() > verify_size { + &expected_data[..verify_size] } else { &expected_data }; @@ -1412,11 +1963,13 @@ impl EmbeddedDebuggerToolHandler { // Perform verification { let mut session = session_arc.session.lock().await; - match crate::flash::FlashManager::verify_flash(&mut session, expected_data, address).await { + match crate::flash::FlashManager::verify_flash(&mut session, expected_data, address) + .await + { Ok(result) => { let message = if result.success { format!( - "✅ Flash verification successful!\n\n\ + "Flash verification successful.\n\n\ Session ID: {}\n\ Address: 0x{:08X}\n\ Bytes Verified: {}\n\n\ @@ -1425,37 +1978,56 @@ impl EmbeddedDebuggerToolHandler { ) } else { let mut message = format!( - "❌ Flash verification failed!\n\n\ + "Flash verification failed.\n\n\ Session ID: {}\n\ Address: 0x{:08X}\n\ Bytes Verified: {}\n\ Mismatches: {}\n\n\ First {} mismatches:\n", - args.session_id, address, result.bytes_verified, result.mismatches.len(), + args.session_id, + address, + result.bytes_verified, + result.mismatches.len(), std::cmp::min(10, result.mismatches.len()) ); - + for (i, mismatch) in result.mismatches.iter().take(10).enumerate() { message.push_str(&format!( " {}. 0x{:08X}: expected 0x{:02X}, got 0x{:02X}\n", - i + 1, mismatch.address, mismatch.expected, mismatch.actual + i + 1, + mismatch.address, + mismatch.expected, + mismatch.actual )); } - + if result.mismatches.len() > 10 { - message.push_str(&format!(" ... and {} more mismatches\n", result.mismatches.len() - 10)); + message.push_str(&format!( + " ... and {} more mismatches\n", + result.mismatches.len() - 10 + )); } - + message }; - - info!("Flash verification completed for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) + + info!( + "Flash verification completed for session: {}", + args.session_id + ); + if result.success { + Ok(CallToolResult::success(vec![Content::text(message)])) + } else { + Ok(CallToolResult::error(vec![Content::text(message)])) + } } Err(e) => { - error!("Flash verification failed for session {}: {}", args.session_id, e); + error!( + "Flash verification failed for session {}: {}", + args.session_id, e + ); let error_msg = format!( - "❌ Flash verification error\n\n\ + "Flash verification error\n\n\ Session ID: {}\n\ Error: {}", args.session_id, e @@ -1466,83 +2038,124 @@ impl EmbeddedDebuggerToolHandler { } } - #[tool(description = "Complete firmware deployment: erase, program, verify, run and attach RTT")] - async fn run_firmware(&self, Parameters(args): Parameters) -> Result { - debug!("Run firmware for session: {}, file: {}", args.session_id, args.file_path); - - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("❌ Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; + #[tool(description = "Firmware deployment helper: erase, program, verify, run, and attach RTT")] + async fn run_firmware( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Run firmware for session: {}, file: {}", + args.session_id, args.file_path + ); + + self.ensure_flash_erase_allowed()?; + let session_arc = self.get_session(&args.session_id).await?; + let file_path = + self.resolve_allowed_file_path(&args.file_path, self.config.flash.max_binary_size)?; let mut status_messages = Vec::new(); let start_time = std::time::Instant::now(); // Step 1: Erase flash - status_messages.push("🔄 Step 1/5: Erasing flash memory...".to_string()); + status_messages.push("Step 1/5: Erasing flash memory...".to_string()); { let mut session = session_arc.session.lock().await; - match crate::flash::FlashManager::erase_flash(&mut session, crate::flash::EraseType::All).await { - Ok(_) => status_messages.push("✅ Flash erased successfully".to_string()), + match crate::flash::FlashManager::erase_flash( + &mut session, + crate::flash::EraseType::All, + ) + .await + { + Ok(_) => status_messages.push("Flash erased successfully".to_string()), Err(e) => { - let error_msg = format!("❌ Flash erase failed: {}", e); + let error_msg = format!("Flash erase failed: {}", e); status_messages.push(error_msg.clone()); - return Err(McpError::internal_error(format!("{}\n\n{}", status_messages.join("\n"), error_msg), None)); + return Err(McpError::internal_error( + format!("{}\n\n{}", status_messages.join("\n"), error_msg), + None, + )); } } } // Step 2: Program firmware - status_messages.push("🔄 Step 2/5: Programming firmware...".to_string()); + status_messages.push("Step 2/5: Programming firmware...".to_string()); let format = match args.format.as_str() { "auto" => crate::flash::FileFormat::Auto, "elf" => crate::flash::FileFormat::Elf, "hex" => crate::flash::FileFormat::Hex, "bin" => crate::flash::FileFormat::Bin, - _ => return Err(McpError::internal_error(format!("Unsupported format: {}", args.format), None)), + _ => { + return Err(McpError::internal_error( + format!("Unsupported format: {}", args.format), + None, + )) + } }; { let mut session = session_arc.session.lock().await; - match crate::flash::FlashManager::program_file(&mut session, std::path::Path::new(&args.file_path), format, None).await { - Ok(result) => status_messages.push(format!("✅ Programmed {} bytes", result.bytes_programmed)), + match crate::flash::FlashManager::program_file( + &mut session, + &file_path, + format, + None, + self.config.flash.verify_after_program, + ) + .await + { + Ok(result) => { + status_messages.push(format!("Programmed {} bytes", result.bytes_programmed)) + } Err(e) => { - let error_msg = format!("❌ Programming failed: {}", e); + let error_msg = format!("Programming failed: {}", e); status_messages.push(error_msg.clone()); - return Err(McpError::internal_error(format!("{}\n\n{}", status_messages.join("\n"), error_msg), None)); + return Err(McpError::internal_error( + format!("{}\n\n{}", status_messages.join("\n"), error_msg), + None, + )); } } } // Step 3: Reset and run if args.reset_after_flash { - status_messages.push("🔄 Step 3/5: Resetting target...".to_string()); + status_messages.push("Step 3/5: Resetting target...".to_string()); { let mut session = session_arc.session.lock().await; let mut core = match session.core(0) { Ok(core) => core, - Err(e) => return Err(McpError::internal_error(format!("Failed to get core: {}", e), None)), + Err(e) => { + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )) + } }; - + match core.reset() { Ok(_) => { - status_messages.push("✅ Target reset successfully".to_string()); + status_messages.push("Target reset successfully".to_string()); // Run the target match core.run() { - Ok(_) => status_messages.push("✅ Target running".to_string()), - Err(e) => warn!("Failed to run after reset: {}", e), + Ok(_) => status_messages.push("Target running".to_string()), + Err(e) => { + let error_msg = format!("Run after reset failed: {}", e); + status_messages.push(error_msg.clone()); + return Err(McpError::internal_error( + format!("{}\n\n{}", status_messages.join("\n"), error_msg), + None, + )); + } } } Err(e) => { - let error_msg = format!("❌ Reset failed: {}", e); + let error_msg = format!("Reset failed: {}", e); status_messages.push(error_msg.clone()); - return Err(McpError::internal_error(format!("{}\n\n{}", status_messages.join("\n"), error_msg), None)); + return Err(McpError::internal_error( + format!("{}\n\n{}", status_messages.join("\n"), error_msg), + None, + )); } } } @@ -1550,110 +2163,144 @@ impl EmbeddedDebuggerToolHandler { // Step 4: Attach RTT (if requested) - Mimic probe-rs run behavior if args.attach_rtt { - status_messages.push("🔄 Step 4/5: Attaching RTT (probe-rs style)...".to_string()); - - // Key improvement: Give target more time to boot, mimic probe-rs run timing - info!("Allowing target firmware to fully initialize RTT control block..."); - tokio::time::sleep(tokio::time::Duration::from_millis(2000)).await; // Initial 2s delay - - // Give target additional time to fully initialize RTT (key improvement) - info!("Giving target additional time to initialize RTT control block..."); - tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; - - // Enhanced RTT retry mechanism with probe-rs style timing + status_messages.push("Step 4/5: Attaching RTT...".to_string()); + + let timeout = tokio::time::Duration::from_millis(args.rtt_timeout_ms as u64); + let started = tokio::time::Instant::now(); let mut rtt_attached = false; - let max_attempts = 8; // Increase retry attempts + let mut last_rtt_error = None; let mut attempt = 1; - - while attempt <= max_attempts && !rtt_attached { - // probe-rs style delay strategy: 1s, 1.5s, 2s, 2.5s, 3s, 3.5s, 4s, 4.5s - let delay_ms = 1000 + (attempt - 1) * 500; - info!("RTT attach attempt {}/{}, waiting {}ms for RTT control block...", attempt, max_attempts, delay_ms); - tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms as u64)).await; - - // Small delay between RTT attempts (let target stabilize) - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - + + loop { + if attempt > 1 { + let remaining = timeout.saturating_sub(started.elapsed()); + if remaining.is_zero() { + break; + } + tokio::time::sleep(remaining.min(tokio::time::Duration::from_millis(500))) + .await; + } + // Try RTT attachment with different strategies (probe-rs style optimization) let mut rtt_manager = session_arc.rtt_manager.lock().await; let rtt_result = match attempt { 1..=2 => { // First 2 attempts: ELF symbol detection (probe-rs priority method) - debug!("RTT attempt {}: Using ELF symbol detection (probe-rs style)", attempt); - rtt_manager.attach_with_elf(session_arc.session.clone(), std::path::Path::new(&args.file_path)).await + debug!( + "RTT attempt {}: Using ELF symbol detection (probe-rs style)", + attempt + ); + rtt_manager + .attach_with_elf(session_arc.session.clone(), &file_path) + .await } 3..=5 => { // Attempts 3-5: standard attach, let probe-rs auto-scan memory debug!("RTT attempt {}: Using standard memory map scan", attempt); - rtt_manager.attach(session_arc.session.clone(), None, None).await + rtt_manager + .attach(session_arc.session.clone(), None, None) + .await } 6..=7 => { // Attempts 6-7: try STM32G4 specific memory ranges - debug!("RTT attempt {}: Using STM32G4 specific memory ranges", attempt); + debug!( + "RTT attempt {}: Using STM32G4 specific memory ranges", + attempt + ); let stm32g4_ranges = vec![ (0x20000000, 0x20004000), // SRAM1 first half: 16KB - most likely RTT location (0x20004000, 0x20008000), // SRAM1 second half: 16KB (0x20008000, 0x2000A000), // SRAM2: 8KB ]; - rtt_manager.attach(session_arc.session.clone(), None, Some(stm32g4_ranges)).await + rtt_manager + .attach(session_arc.session.clone(), None, Some(stm32g4_ranges)) + .await } _ => { // Last attempt: try common RTT control block addresses let cb_addr = 0x20000000; - debug!("RTT attempt {}: Using specific control block address 0x{:08X}", attempt, cb_addr); - rtt_manager.attach(session_arc.session.clone(), Some(cb_addr), None).await + debug!( + "RTT attempt {}: Using specific control block address 0x{:08X}", + attempt, cb_addr + ); + rtt_manager + .attach(session_arc.session.clone(), Some(cb_addr), None) + .await } }; - + match rtt_result { Ok(_) => { let up_channels = rtt_manager.up_channel_count(); let down_channels = rtt_manager.down_channel_count(); - status_messages.push(format!("✅ RTT attached on attempt {} ({} up, {} down channels)", attempt, up_channels, down_channels)); + status_messages.push(format!( + "RTT attached on attempt {} ({} up, {} down channels)", + attempt, up_channels, down_channels + )); info!("RTT successfully attached after {} attempts!", attempt); rtt_attached = true; + break; } Err(e) => { - if attempt == max_attempts { - // Final attempt failed - status_messages.push(format!("⚠️ RTT attach failed after {} attempts: {}", max_attempts, e)); - warn!("RTT attachment failed completely after {} attempts", max_attempts); - } else { - debug!("RTT attach attempt {}/{} failed: {}, retrying with different strategy...", attempt, max_attempts, e); - } + debug!("RTT attach attempt {} failed: {}", attempt, e); + last_rtt_error = Some(e.to_string()); } } + + if args.rtt_timeout_ms == 0 || started.elapsed() >= timeout { + break; + } attempt += 1; } - - // If RTT successfully connected, give extra initialization time - if rtt_attached { - info!("RTT connected successfully, allowing channel stabilization..."); - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + if !rtt_attached { + let error_msg = format!( + "RTT attach failed within {}ms after {} attempt(s): {}", + args.rtt_timeout_ms, + attempt, + last_rtt_error.unwrap_or_else(|| "timeout expired".to_string()) + ); + status_messages.push(error_msg.clone()); + warn!("{}", error_msg); + return Err(McpError::internal_error( + format!("{}\n\n{}", status_messages.join("\n"), error_msg), + None, + )); } + + info!("RTT connected successfully, allowing channel stabilization..."); + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; } - status_messages.push("🔄 Step 5/5: Finalizing...".to_string()); + status_messages.push("Step 5/5: Finalizing...".to_string()); let elapsed = start_time.elapsed(); let message = format!( - "🚀 Firmware deployment completed!\n\n\ + "Firmware deployment completed.\n\n\ Session ID: {}\n\ File: {}\n\ Format: {}\n\ Total Time: {:.1}s\n\n\ Status:\n{}\n\n\ - ✅ Firmware is now running on target.\n\ + Firmware is now running on target.\n\ {}", args.session_id, - args.file_path, + file_path.display(), args.format, elapsed.as_secs_f64(), status_messages.join("\n"), - if args.attach_rtt { "Use 'rtt_read' to monitor target output." } else { "Use 'rtt_attach' to enable real-time communication." } + if args.attach_rtt { + "Use 'rtt_read' to monitor target output." + } else { + "Use 'rtt_attach' to enable real-time communication." + } ); - info!("Firmware deployment completed for session: {} in {:.1}s", args.session_id, elapsed.as_secs_f64()); + info!( + "Firmware deployment completed for session: {} in {:.1}s", + args.session_id, + elapsed.as_secs_f64() + ); Ok(CallToolResult::success(vec![Content::text(message)])) } } @@ -1665,12 +2312,12 @@ impl EmbeddedDebuggerToolHandler { /// Parse address string (hex or decimal) to u64 fn parse_address(addr_str: &str) -> Result { let addr_str = addr_str.trim(); - + if addr_str.starts_with("0x") || addr_str.starts_with("0X") { - u64::from_str_radix(&addr_str[2..], 16) - .map_err(|e| format!("Invalid hex address: {}", e)) + u64::from_str_radix(&addr_str[2..], 16).map_err(|e| format!("Invalid hex address: {}", e)) } else { - addr_str.parse::() + addr_str + .parse::() .map_err(|e| format!("Invalid decimal address: {}", e)) } } @@ -1680,14 +2327,17 @@ fn parse_data(data_str: &str, format: &str) -> Result, String> { match format { "hex" => { // Remove spaces and 0x prefixes - let clean_str = data_str.replace(" ", "").replace("0x", "").replace("0X", ""); - if clean_str.len() % 2 != 0 { + let clean_str = data_str + .replace(" ", "") + .replace("0x", "") + .replace("0X", ""); + if (clean_str.len() & 1) != 0 { return Err("Hex data must have even number of characters".to_string()); } - + (0..clean_str.len()) .step_by(2) - .map(|i| u8::from_str_radix(&clean_str[i..i+2], 16)) + .map(|i| u8::from_str_radix(&clean_str[i..i + 2], 16)) .collect::, _>>() .map_err(|e| format!("Invalid hex data: {}", e)) } @@ -1703,7 +2353,7 @@ fn parse_data(data_str: &str, format: &str) -> Result, String> { } }) .collect(); - + match words { Ok(words) => { let mut data = Vec::new(); @@ -1726,7 +2376,7 @@ fn parse_data(data_str: &str, format: &str) -> Result, String> { } }) .collect(); - + match words { Ok(words) => { let mut data = Vec::new(); @@ -1750,19 +2400,21 @@ fn format_memory_data(data: &[u8], format: &str, base_address: u64) -> String { for (i, chunk) in data.chunks(16).enumerate() { let addr = base_address + (i * 16) as u64; result.push_str(&format!("0x{:08X}: ", addr)); - + // Hex bytes for (j, byte) in chunk.iter().enumerate() { - if j == 8 { result.push(' '); } + if j == 8 { + result.push(' '); + } result.push_str(&format!("{:02X} ", byte)); } - + // Pad if needed if chunk.len() < 16 { let padding = (16 - chunk.len()) * 3 + (if chunk.len() <= 8 { 1 } else { 0 }); result.push_str(&" ".repeat(padding)); } - + // ASCII representation result.push_str("| "); for byte in chunk { @@ -1776,12 +2428,11 @@ fn format_memory_data(data: &[u8], format: &str, base_address: u64) -> String { } result } - "binary" => { - data.iter() - .map(|b| format!("{:08b}", b)) - .collect::>() - .join(" ") - } + "binary" => data + .iter() + .map(|b| format!("{:08b}", b)) + .collect::>() + .join(" "), "words32" => { let mut result = String::new(); for (i, chunk) in data.chunks(4).enumerate() { @@ -1804,9 +2455,7 @@ fn format_memory_data(data: &[u8], format: &str, base_address: u64) -> String { } result } - "ascii" => { - String::from_utf8_lossy(data).to_string() - } + "ascii" => String::from_utf8_lossy(data).to_string(), _ => { // Default to hex if unknown format format_memory_data(data, "hex", base_address) @@ -1821,7 +2470,7 @@ impl ServerHandler for EmbeddedDebuggerToolHandler { protocol_version: ProtocolVersion::V_2024_11_05, capabilities: ServerCapabilities::builder().enable_tools().build(), server_info: Implementation::from_build_env(), - instructions: Some("Complete embedded debugging and flash programming MCP server supporting ARM Cortex-M, RISC-V, and other architectures via probe-rs. Provides comprehensive debugging and flash programming capabilities including probe detection, target connection, memory operations, breakpoints, RTT communication, and flash programming with real hardware integration. All 22 tools available: list_probes, connect, disconnect, probe_info, halt, run, reset, step, get_status, read_memory, write_memory, set_breakpoint, clear_breakpoint, rtt_attach, rtt_detach, rtt_read, rtt_write, rtt_channels, flash_erase, flash_program, flash_verify, run_firmware.".to_string()), + instructions: Some("Embedded debugging and flash programming MCP server for ARM Cortex-M, RISC-V, and other probe-rs-supported targets. Exposes 22 tools for probe detection, target sessions, memory operations, breakpoints, RTT communication, and flash programming: list_probes, connect, disconnect, probe_info, halt, run, reset, step, get_status, read_memory, write_memory, set_breakpoint, clear_breakpoint, rtt_attach, rtt_detach, rtt_read, rtt_write, rtt_channels, flash_erase, flash_program, flash_verify, run_firmware.".to_string()), } } @@ -1830,7 +2479,7 @@ impl ServerHandler for EmbeddedDebuggerToolHandler { _request: InitializeRequestParam, _context: RequestContext, ) -> Result { - info!("Complete Embedded Debugger MCP server initialized with all 22 tools (18 debug + 4 flash)"); + info!("Embedded Debugger MCP server initialized with 22 tools (18 debug + 4 flash)"); Ok(self.get_info()) } -} \ No newline at end of file +} diff --git a/src/tools/mod.rs b/src/tools/mod.rs index bf46ab1..ad0c061 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -1,5 +1,5 @@ //! Embedded debugger MCP tools module -//! +//! //! This module provides a unified tool handler for all embedded debugging operations //! using the RMCP 0.3.2 API patterns, similar to the serial-mcp-rs implementation. @@ -9,4 +9,4 @@ pub mod types; // Export all 18 tools (13 base debugging + 5 RTT communication) pub use debugger_tools::*; -pub use types::*; \ No newline at end of file +pub use types::*; diff --git a/src/tools/types.rs b/src/tools/types.rs index de84d09..ce35e62 100644 --- a/src/tools/types.rs +++ b/src/tools/types.rs @@ -1,7 +1,7 @@ //! Type definitions for embedded debugger MCP tools -use serde::{Deserialize, Serialize}; use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; // ============================================================================= // Debugger Management Types @@ -29,8 +29,12 @@ pub struct ConnectArgs { pub halt_after_connect: bool, } -fn default_speed_khz() -> u32 { 4000 } -fn default_true() -> bool { true } +fn default_speed_khz() -> u32 { + 4000 +} +fn default_true() -> bool { + true +} #[derive(Debug, Deserialize, JsonSchema)] pub struct DisconnectArgs { @@ -72,7 +76,9 @@ pub struct ResetArgs { pub halt_after_reset: bool, } -fn default_reset_type() -> String { "hardware".to_string() } +fn default_reset_type() -> String { + "hardware".to_string() +} #[derive(Debug, Deserialize, JsonSchema)] pub struct StepArgs { @@ -103,7 +109,9 @@ pub struct ReadMemoryArgs { pub format: String, } -fn default_format() -> String { "hex".to_string() } +fn default_format() -> String { + "hex".to_string() +} #[derive(Debug, Deserialize, JsonSchema)] pub struct WriteMemoryArgs { @@ -118,7 +126,6 @@ pub struct WriteMemoryArgs { pub format: String, } - // ============================================================================= // Breakpoint Management Types // ============================================================================= @@ -134,7 +141,9 @@ pub struct SetBreakpointArgs { pub breakpoint_type: String, } -fn default_breakpoint_type() -> String { "hardware".to_string() } +fn default_breakpoint_type() -> String { + "hardware".to_string() +} #[derive(Debug, Deserialize, JsonSchema)] pub struct ClearBreakpointArgs { @@ -144,13 +153,10 @@ pub struct ClearBreakpointArgs { pub address: String, } - // ============================================================================= // Flash Programming Types // ============================================================================= - - // ============================================================================= // New Flash Programming Types // ============================================================================= @@ -168,7 +174,9 @@ pub struct FlashEraseArgs { pub size: Option, } -fn default_erase_all() -> String { "all".to_string() } +fn default_erase_all() -> String { + "all".to_string() +} #[derive(Debug, Deserialize, JsonSchema)] pub struct FlashProgramArgs { @@ -186,7 +194,9 @@ pub struct FlashProgramArgs { pub verify: bool, } -fn default_auto_format() -> String { "auto".to_string() } +fn default_auto_format() -> String { + "auto".to_string() +} #[derive(Debug, Deserialize, JsonSchema)] pub struct FlashVerifyArgs { @@ -222,7 +232,9 @@ pub struct RunFirmwareArgs { pub rtt_timeout_ms: u32, } -fn default_rtt_timeout() -> u32 { 3000 } +fn default_rtt_timeout() -> u32 { + 3000 +} // ============================================================================= // RTT Communication Types @@ -266,8 +278,12 @@ pub struct RttReadArgs { pub timeout_ms: u64, } -fn default_max_bytes() -> usize { 1024 } -fn default_timeout_ms() -> u64 { 1000 } +fn default_max_bytes() -> usize { + 1024 +} +fn default_timeout_ms() -> u64 { + 1000 +} #[derive(Debug, Deserialize, JsonSchema)] pub struct RttWriteArgs { @@ -283,7 +299,9 @@ pub struct RttWriteArgs { pub encoding: String, } -fn default_encoding() -> String { "utf8".to_string() } +fn default_encoding() -> String { + "utf8".to_string() +} #[derive(Debug, Deserialize, JsonSchema)] pub struct RttChannelsArgs { @@ -368,4 +386,4 @@ pub struct RttChannelInfo { pub direction: String, // "up", "down" pub buffer_size: usize, pub flags: u32, -} \ No newline at end of file +} diff --git a/src/utils.rs b/src/utils.rs index 86759dd..c56679c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -36,4 +36,4 @@ impl std::fmt::Display for ProbeType { ProbeType::Unknown => write!(f, "Unknown"), } } -} \ No newline at end of file +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index cd67c04..cfa4e34 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -6,7 +6,7 @@ use embedded_debugger_mcp::Config; async fn test_config_validation() { let config = Config::default(); assert!(config.validate().is_ok()); - + // Test TOML serialization let toml_str = config.to_toml().unwrap(); assert!(!toml_str.is_empty()); @@ -18,10 +18,10 @@ async fn test_config_validation() { async fn test_probe_discovery() { // Test probe discovery (this will work even without hardware) use embedded_debugger_mcp::debugger::discovery::ProbeDiscovery; - + let result = ProbeDiscovery::list_probes(); assert!(result.is_ok()); - + // The result might be empty if no probes are connected, which is fine let probes = result.unwrap(); println!("Found {} probes", probes.len()); @@ -30,10 +30,10 @@ async fn test_probe_discovery() { #[test] fn test_error_types() { use embedded_debugger_mcp::DebugError; - + let error = DebugError::ProbeNotFound("test".to_string()); assert!(error.to_string().contains("Probe not found")); - + let error = DebugError::SessionLimitExceeded(5); assert!(error.to_string().contains("Session limit exceeded")); } @@ -41,16 +41,16 @@ fn test_error_types() { #[test] fn test_probe_type_detection() { use embedded_debugger_mcp::utils::ProbeType; - + // Test J-Link detection assert_eq!(ProbeType::from_vid_pid(0x1366, 0x0101), ProbeType::JLink); - + // Test ST-Link detection assert_eq!(ProbeType::from_vid_pid(0x0483, 0x374B), ProbeType::StLink); - + // Test DAPLink detection assert_eq!(ProbeType::from_vid_pid(0x0D28, 0x0204), ProbeType::DapLink); - + // Test unknown probe assert_eq!(ProbeType::from_vid_pid(0xFFFF, 0xFFFF), ProbeType::Unknown); } @@ -59,12 +59,12 @@ fn test_probe_type_detection() { async fn test_mcp_tool_handler() { // Test the main MCP tool handler use embedded_debugger_mcp::EmbeddedDebuggerToolHandler; - + let _handler = EmbeddedDebuggerToolHandler::new(10); - + // Test that we can create multiple handlers (should work fine) let _handler2 = EmbeddedDebuggerToolHandler::new(5); - + // Verify the handler was created - this is more meaningful than just instantiation println!("MCP tool handler created and ready for use"); -} \ No newline at end of file +} From 320173d8f1003c597e02fb6c051f1179be6835b7 Mon Sep 17 00:00:00 2001 From: Hongda Chen <54146249+Adancurusul@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:34:57 +0800 Subject: [PATCH 2/8] fix: close release hardening checkpoint gaps --- .github/workflows/ci.yml | 14 + README.md | 2 +- README_zh.md | 2 +- .../docs/ALL_22_TOOLS_TESTING_SUMMARY.md | 596 ++---------------- .../docs/RTT_BIDIRECTIONAL_DESIGN.md | 373 ++--------- .../STM32_demo/docs/RTT_TESTING_RESULTS.md | 452 ++----------- src/config.rs | 76 ++- src/main.rs | 42 +- src/tools/debugger_tools.rs | 222 ++++++- tests/integration_tests.rs | 40 ++ 10 files changed, 488 insertions(+), 1331 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6863ab9..c7a3a07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,20 @@ jobs: - name: Package run: cargo package --locked + - name: CLI smoke + run: | + cargo run --locked -- --help > /tmp/embedded-debugger-help.txt + cargo run --locked -- doctor --json > /tmp/embedded-debugger-doctor.json + python3 -m json.tool /tmp/embedded-debugger-doctor.json > /dev/null + cargo run --locked -- probes list --json > /tmp/embedded-debugger-probes.json + python3 -m json.tool /tmp/embedded-debugger-probes.json > /dev/null + cargo run --locked -- skill print-prompt > /tmp/embedded-debugger-skill.txt + grep -q "embedded-debugger skill" /tmp/embedded-debugger-skill.txt + if cargo run --locked -- not-a-command > /tmp/embedded-debugger-invalid.out 2> /tmp/embedded-debugger-invalid.err; then + echo "invalid subcommand unexpectedly succeeded" + exit 1 + fi + - name: Validate bundled skill run: python3 .github/scripts/validate_skill.py skills/embedded-debugger diff --git a/README.md b/README.md index 07d5fee..7ac03ac 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ cargo clippy --locked --all-targets --all-features -- -D warnings cargo test --locked --all-targets --all-features RUSTDOCFLAGS="-D warnings" cargo doc --locked --all-features --no-deps cargo package --locked -python3 /Users/adan/.codex/skills/.system/skill-creator/scripts/quick_validate.py skills/embedded-debugger +python3 .github/scripts/validate_skill.py skills/embedded-debugger (cd examples/STM32_demo && CARGO_TARGET_DIR=/tmp/embedded-debugger-mcp-stm32-target cargo +nightly check --locked) ``` diff --git a/README_zh.md b/README_zh.md index aef4021..225ee6a 100644 --- a/README_zh.md +++ b/README_zh.md @@ -190,7 +190,7 @@ cargo clippy --locked --all-targets --all-features -- -D warnings cargo test --locked --all-targets --all-features RUSTDOCFLAGS="-D warnings" cargo doc --locked --all-features --no-deps cargo package --locked -python3 /Users/adan/.codex/skills/.system/skill-creator/scripts/quick_validate.py skills/embedded-debugger +python3 .github/scripts/validate_skill.py skills/embedded-debugger (cd examples/STM32_demo && CARGO_TARGET_DIR=/tmp/embedded-debugger-mcp-stm32-target cargo +nightly check --locked) ``` diff --git a/examples/STM32_demo/docs/ALL_22_TOOLS_TESTING_SUMMARY.md b/examples/STM32_demo/docs/ALL_22_TOOLS_TESTING_SUMMARY.md index d874f8c..57cb314 100644 --- a/examples/STM32_demo/docs/ALL_22_TOOLS_TESTING_SUMMARY.md +++ b/examples/STM32_demo/docs/ALL_22_TOOLS_TESTING_SUMMARY.md @@ -1,562 +1,68 @@ -# Complete MCP Embedded Debugger - All 22 Tools Testing Summary +# MCP Tool Hardware Validation Notes -## Overview +This file records a historical hardware validation pass for the STM32 RTT demo. +It is not a release guarantee and should not be treated as evidence for every +probe, target, operating system, or firmware image. -This document provides a comprehensive testing summary of all 22 MCP embedded debugger tools using the new bidirectional RTT demo firmware. All tests were conducted on STM32G431CBTx with ST-Link V2 probe, demonstrating complete functionality across probe management, memory operations, debugging controls, breakpoints, flash operations, RTT communication, and session management. +## Scope -## Test Environment +- Target used: STM32G431CBTx +- Probe used: ST-Link V2 +- Firmware: `examples/STM32_demo` +- Host interface: embedded-debugger-mcp MCP tools +- Main focus: probe discovery, target session lifecycle, memory operations, + breakpoints, flash operations, RTT communication, and session cleanup -- **Target**: STM32G431CBTx microcontroller -- **Debug Probe**: ST-Link V2 (VID:PID = 0483:3748) -- **Firmware**: demo_code - RTT Bidirectional Demo (warning-free compilation) -- **MCP Server**: embedded-debugger-mcp v0.1.0 -- **Test Date**: 2025-08-05 +## Tool Areas Covered -## Complete Tool Testing Results +| Area | Notes | +|------|-------| +| Probe management | Probe listing, session connection, and probe information were exercised. | +| Memory operations | Read/write flows were exercised against the validation target. | +| Debug control | Halt, run, reset, and step flows were exercised. | +| Breakpoints | Hardware breakpoint set and clear flows were exercised. | +| Flash operations | Program, erase, and verify flows were exercised on the validation target. | +| RTT communication | Up-channel reads, down-channel writes, channel listing, attach, and detach were exercised. | +| Session management | Disconnect and reconnect flows were exercised. | -### ✅ 22/22 Tools - 100% Success Rate +## Important Limits -| Category | Tools | Success Rate | Notes | -|----------|-------|--------------|-------| -| **Probe Management** | 3/3 | 100% | All probe operations working | -| **Memory Operations** | 2/2 | 100% | Read/write with verification | -| **Debug Control** | 4/4 | 100% | Full execution control | -| **Breakpoint Management** | 2/2 | 100% | Hardware breakpoints functional | -| **Flash Operations** | 3/3 | 100% | Programming, erase, verification | -| **RTT Communication** | 6/6 | 100% | **Full bidirectional RTT** | -| **Session Management** | 2/2 | 100% | Clean connect/disconnect | +- Results are hardware-specific and should be re-run for each release candidate. +- Flash and memory operations are destructive on real targets; use an explicit + config that enables the requested operation. +- Current release checks verify build, package, CLI, skill, and demo firmware + compilation. They do not replay this full hardware validation automatically. +- Do not infer device success from this document alone. Record fresh command or + MCP tool output when validating a board. ---- +## Reproducible Baseline -## Detailed Test Results by Category +Build the demo firmware: -### 1. Probe Management Tools (3 tools) - -#### 1.1 list_probes -**Status: ✅ PASS** - -``` -Found 1 debug probe(s): - -1. STLink V2 - VID:PID = 0483:3748 - Probe Type: "ST-LINK" -``` - -**Result**: Perfect probe detection and identification. - -#### 1.2 probe_info -**Status: ✅ PASS** - -``` -📊 Debug Session Information - -Probe Information: -- Identifier: STLink V2 -- Connected: true - -Target Information: -- Chip: STM32G431CBTx - -Session Status: -- Session ID: session_1754377337779 -- Created: 2025-08-05 07:02:17 UTC -- Duration: 53.3 minutes - -Session is active and ready for operations. -``` - -**Result**: Comprehensive session information retrieval successful. - -#### 1.3 get_status -**Status: ✅ PASS** - -``` -📊 Debug Session Status - -Core Information: -- PC: 0x00000000 -- SP: 0x00000000 -- State: Running -- Halt reason: Unknown - -Session Information: -- ID: session_1754377337779 -- Connected: true -- Target: STM32G431CBTx -- Probe: STLink V2 -- Duration: 53.3 minutes -``` - -**Result**: Real-time status monitoring working correctly. - -### 2. Memory Operations (2 tools) - -#### 2.1 read_memory -**Status: ✅ PASS** - -``` -📖 Memory read completed successfully! - -Address: 0x08000000 -Size: 64 bytes -Format: hex - -Data: -0x08000000: 00 80 00 20 D9 01 00 08 F9 1F 00 08 9D 31 00 08 | ... .........1.. -0x08000010: F9 1F 00 08 F9 1F 00 08 F9 1F 00 08 00 00 00 00 | ................ -0x08000020: 00 00 00 00 00 00 00 00 00 00 00 00 F9 1F 00 08 | ................ -0x08000030: F9 1F 00 08 00 00 00 00 F9 1F 00 08 F9 1F 00 08 | ................ -``` - -**Result**: Flash memory reading with proper hex formatting. - -#### 2.2 write_memory -**Status: ✅ PASS** - -``` -✏️ Memory write completed successfully! - -Address: 0x20007F00 -Data: CAFEBABE -Format: hex -Bytes written: 4 -``` - -**Result**: RAM memory write operation successful with verification. - -### 3. Debug Control Tools (4 tools) - -#### 3.1 halt -**Status: ✅ PASS** - -``` -✅ Target halted successfully! - -PC: 0x0800129E -SP: 0x20007FD8 -State: Halted -``` - -**Result**: Target halted with register state captured. - -#### 3.2 step -**Status: ✅ PASS** - -``` -✅ Single step completed successfully! - -PC: 0x0800127A (changed from 0x0800129E) -SP: 0x20007FD8 -State: Halted -``` - -**Result**: Single instruction execution with PC advancement. - -#### 3.3 run -**Status: ✅ PASS** - -``` -✅ Target resumed execution successfully! - -Status: Running - -The target is now executing code. Use 'halt' to stop execution. -``` - -**Result**: Target resumed execution successfully. - -#### 3.4 reset -**Status: ✅ PASS** - -``` -✅ Target reset completed successfully! - -Reset type: hardware -Halted after reset: false -PC: 0x00000000 -SP: 0x00000000 -State: Running -``` - -**Result**: Hardware reset executed with target running. - -### 4. Breakpoint Management (2 tools) - -#### 4.1 set_breakpoint -**Status: ✅ PASS** - -``` -🎯 Breakpoint set successfully! - -Address: 0x080012A0 -Type: Hardware breakpoint - -The target will halt when execution reaches this address. -``` - -**Result**: Hardware breakpoint set successfully. - -#### 4.2 clear_breakpoint -**Status: ✅ PASS** - -``` -🎯 Breakpoint cleared successfully! - -Address: 0x080012A0 - -The breakpoint has been removed. -``` - -**Result**: Breakpoint cleared successfully. - -### 5. Flash Operations (3 tools) - -#### 5.1 flash_program -**Status: ✅ PASS** - -``` -✅ Flash programming completed successfully! - -File: G:\codes\MCP_server\demo_code\target\thumbv7em-none-eabi\release\demo_code -Format: elf -Bytes Programmed: 510636 -Duration: 1316ms -Verification: ✅ Passed - -Firmware has been programmed to flash memory. -``` - -**Result**: ELF firmware programming with internal verification successful. - -#### 5.2 flash_erase -**Status: ✅ PASS** - -``` -✅ Flash erase completed successfully! - -Erase Type: all -Duration: 121ms -Full chip erased - -Flash memory has been erased and is ready for programming. -``` - -**Result**: Full chip erase completed in 121ms. - -#### 5.3 flash_verify -**Status: ✅ PASS (Expected differences)** - -``` -❌ Flash verification failed! - -Bytes Verified: 16 -Mismatches: 2 - -First 2 mismatches: - 1. 0x08000004: expected 0x01, got 0xD9 - 2. 0x08000005: expected 0xD9, got 0x01 -``` - -**Result**: Verification function working correctly, detecting byte-order differences as expected. - -### 6. RTT Communication Tools (6 tools) - **🌟 MAJOR IMPROVEMENT** - -#### 6.1 rtt_channels -**Status: ✅ PASS** - -``` -📋 RTT Channels - -📥 Up Channels (Target → Host): - 0. Terminal (Size: 1024 bytes, Mode: RTT) - 2. Debug (Size: 256 bytes, Mode: RTT) - 1. Data (Size: 512 bytes, Mode: RTT) - -📤 Down Channels (Host → Target): - 0. Commands (Size: 64 bytes, Mode: RTT) - 1. Config (Size: 128 bytes, Mode: RTT) -``` - -**Result**: ✅ **Perfect bidirectional channel detection - 3 up + 2 down channels** - -#### 6.2 rtt_read -**Status: ✅ PASS** - -**Terminal Channel (Up 0)**: -``` -📥 RTT Read from Channel 0 - -Bytes Read: 516 - -Data: -Text: === RTT Bidirectional Communication Demo === -Target: STM32G431CBTx -Channels: 3 Up (Terminal/Data/Debug), 2 Down (Commands/Config) - -Available Commands (send to down channel 0): - L - Toggle LED - R - Reset Fibonacci counter - S - Start/Stop calculation - F - Get current Fibonacci value - I - System information - P - Pause 5 seconds - 0-9 - Set speed (1x-10x) - -Config Commands (send to down channel 1): - SPEED:n, LED:ON/OFF, MODE:AUTO/MANUAL, RESET - -Starting main loop... -Milestone: 10 Fibonacci numbers calculated -``` - -**Result**: ✅ **Rich text-based communication with complete system information** - -#### 6.3 rtt_write - **🎉 NOW WORKING!** -**Status: ✅ PASS** - -``` -📤 RTT Write to Channel 0 - -Data: F -Encoding: utf8 -Bytes Written: 1 - -Data sent successfully to target. -``` - -**Result**: ✅ **Bidirectional RTT write now fully functional!** - -#### 6.4 rtt_attach -**Status: ✅ PASS** - -``` -✅ RTT attached on attempt 1 (3 up, 2 down channels) -``` - -**Result**: ✅ **First-attempt RTT connection with ELF symbol detection** - -#### 6.5 rtt_detach -**Status: ✅ PASS** - -``` -✅ RTT detached successfully - -RTT communication has been closed. -``` - -**Result**: ✅ **Clean RTT disconnection** - -#### 6.6 run_firmware (includes RTT) -**Status: ✅ PASS** - -``` -🚀 Firmware deployment completed! - -Total Time: 6.1s - -Status: -🔄 Step 1/5: Erasing flash memory... -✅ Flash erased successfully -🔄 Step 2/5: Programming firmware... -✅ Programmed 510636 bytes -🔄 Step 3/5: Resetting target... -✅ Target reset successfully -✅ Target running -🔄 Step 4/5: Attaching RTT (probe-rs style)... -✅ RTT attached on attempt 1 (3 up, 2 down channels) -🔄 Step 5/5: Finalizing... - -✅ Firmware is now running on target. -``` - -**Result**: ✅ **Complete firmware deployment with automatic RTT attachment** - -### 7. Session Management (2 tools) - -#### 7.1 disconnect -**Status: ✅ PASS** - -``` -✅ Debug session disconnected successfully - -Session ID: session_1754377337779 -Probe: STLink V2 -Target: STM32G431CBTx -Duration: 55.1 minutes - -probe-rs Session resources have been cleaned up. -``` - -**Result**: ✅ **Clean session termination with resource cleanup** - -#### 7.2 connect -**Status: ✅ PASS** - -``` -✅ Debug session established! - -Session ID: session_1754380648429 -Probe: STLink V2 (VID:PID = 0483:3748) -Target: STM32G431CBTx -Connected at: 2025-08-05 07:57:28 UTC - -Target connection established and ready for debugging. +```bash +cd examples/STM32_demo +CARGO_TARGET_DIR=/tmp/embedded-debugger-mcp-stm32-target cargo +nightly check --locked ``` -**Result**: ✅ **New session established with fresh session ID** +Run host-side release checks from the repository root: ---- - -## Bidirectional RTT Testing - Interactive Commands - -### Command Interface Validation - -| Command | Channel | Input | Response | Status | -|---------|---------|-------|----------|---------| -| LED Toggle | Down 0 | `L` | "LED toggled: true" | ✅ Working | -| Fibonacci Query | Down 0 | `F` | Current Fibonacci value | ✅ Working | -| System Info | Down 0 | `I` | System details | ✅ Working | -| Speed Config | Down 1 | `SPEED:3` | "Speed configured to: 3x" | ✅ Working | -| Reset | Down 0 | `R` | Counter reset + restart | ✅ Working | - -### Real-time Data Streaming - -**Data Channel (Up 1) - Fibonacci Stream**: -``` -F(0) = 0 -F(1) = 1 -F(2) = 1 -F(3) = 2 -F(4) = 3 -F(5) = 5 -F(6) = 8 -F(7) = 13 -...continuous stream... +```bash +cargo fmt --all -- --check +cargo clippy --locked --all-targets --all-features -- -D warnings +cargo test --locked --all-targets --all-features +RUSTDOCFLAGS="-D warnings" cargo doc --locked --all-features --no-deps +cargo package --locked +embedded-debugger-mcp doctor --json +embedded-debugger-mcp probes list --json +embedded-debugger-mcp skill print-prompt ``` -**Debug Channel (Up 2) - System Status**: -``` -Status: Loop#50, Speed:1x, LED:true, Calc:true, FibIdx:50 -``` - ---- - -## Performance Analysis - -### Connection Performance -- **RTT Attachment**: First attempt success (100% reliability) -- **Session Establishment**: <1 second -- **Command Response**: <10ms latency -- **Data Streaming**: Continuous without loss - -### Memory Operations -- **Flash Programming**: 510,636 bytes in 1.3 seconds -- **Flash Erase**: Full chip in 121ms -- **Memory Read/Write**: <50ms operations - -### System Stability -- **Session Duration**: 55+ minutes continuous operation -- **Tool Success Rate**: 22/22 (100%) -- **Error Rate**: 0% (no failures) -- **Resource Management**: Clean cleanup on disconnect - -## Major Improvements from Previous Testing - -| Aspect | Previous | Current | Improvement | -|---------|----------|---------|-------------| -| **RTT Channels** | 1 up, 0 down | 3 up, 2 down | 🚀 **400% increase** | -| **RTT Write** | ❌ Failed | ✅ Working | 🎉 **Complete fix** | -| **Bidirectional** | ❌ No | ✅ Full | ⭐ **New capability** | -| **Interactive Control** | ❌ None | ✅ Real-time | 🔥 **Game changer** | -| **Command Interface** | ❌ None | ✅ Rich set | 💯 **Professional level** | -| **Data Streaming** | Basic logging | Multi-channel | 📈 **Enhanced** | - -## Key Technical Achievements - -### 1. Complete RTT Bidirectional Implementation -- ✅ **3 Up Channels**: Terminal, Data, Debug -- ✅ **2 Down Channels**: Commands, Config -- ✅ **Real-time Processing**: Immediate command response -- ✅ **Multi-channel Coordination**: Structured data flow - -### 2. MCP Tool Ecosystem Maturity -- ✅ **100% Tool Coverage**: All 22 tools functional -- ✅ **Zero Failures**: No broken functionality -- ✅ **Production Stability**: 55+ minute continuous operation -- ✅ **Professional Performance**: Sub-second operations - -### 3. Interactive Debugging Capability -- ✅ **Command Interface**: 8+ interactive commands -- ✅ **Configuration Management**: Runtime parameter changes -- ✅ **Status Monitoring**: Real-time system feedback -- ✅ **Data Streaming**: Continuous Fibonacci calculations - -### 4. Hardware Validation Excellence -- ✅ **STM32G431CBTx**: Complete target validation -- ✅ **ST-Link V2**: Full probe functionality -- ✅ **Real Hardware**: No simulation or emulation -- ✅ **Production Environment**: Actual embedded development workflow - -## Comparison with Industry Standards - -### vs. OpenOCD -- ✅ **Better RTT Integration**: Native bidirectional support -- ✅ **Easier Setup**: Single MCP server interface -- ✅ **Better Performance**: First-attempt RTT connection - -### vs. SEGGER J-Link -- ✅ **Open Source**: No licensing restrictions -- ✅ **Multi-probe Support**: Works with ST-Link, J-Link, etc. -- ✅ **API Integration**: MCP protocol for AI/automation - -### vs. cargo-embed -- ✅ **Enhanced Functionality**: 22 comprehensive tools -- ✅ **Better Integration**: Single interface for all operations -- ✅ **Production Ready**: Robust error handling - -## Future Expansion Possibilities - -### Additional Target Support -- ✅ **Current**: STM32G431CBTx validated -- 🎯 **Future**: Other STM32 families, nRF52, ESP32, RISC-V - -### Enhanced RTT Features -- ✅ **Current**: 5-channel bidirectional -- 🎯 **Future**: Binary protocols, custom formatters, streaming APIs - -### Advanced Debugging -- ✅ **Current**: Basic breakpoints, memory ops -- 🎯 **Future**: Watchpoints, trace, profiling - -## Conclusion - -### 🏆 Complete Success - Production Ready - -The MCP Embedded Debugger has achieved **100% functionality** with all 22 tools working flawlessly on real hardware. The bidirectional RTT implementation represents a significant advancement, transforming the system from a basic debugging tool into a comprehensive interactive embedded development platform. - -### 📊 Final Assessment - -**Status: ✅ FULLY FUNCTIONAL - PRODUCTION DEPLOYMENT READY** - -### Key Success Metrics: -- **✅ Tool Coverage**: 22/22 (100%) -- **✅ RTT Bidirectional**: Full implementation -- **✅ Hardware Validation**: Real STM32G431CBTx -- **✅ Stability**: 55+ minutes continuous operation -- **✅ Performance**: Sub-second operations -- **✅ Reliability**: Zero failures - -### 🚀 Ready for: -- **Production embedded development workflows** -- **AI-assisted debugging and automation** -- **Educational and training environments** -- **Industrial IoT development** -- **Rapid prototyping and testing** - -The MCP Embedded Debugger now stands as a **world-class embedded debugging solution** with capabilities that match or exceed commercial alternatives, while providing the flexibility and extensibility of an open-source MCP-based architecture. +## Follow-Up Validation ---- +For a new target or release candidate, capture: -*Comprehensive testing completed on 2025-08-05* -*All 22 tools validated with bidirectional RTT on STM32G431CBTx hardware* \ No newline at end of file +- exact binary or firmware path +- config file used for flash, memory, and file-path permissions +- probe identifier and target chip name +- command or MCP tool outputs for every destructive operation +- failure output when a forbidden operation is correctly rejected diff --git a/examples/STM32_demo/docs/RTT_BIDIRECTIONAL_DESIGN.md b/examples/STM32_demo/docs/RTT_BIDIRECTIONAL_DESIGN.md index 010283d..7baaed8 100644 --- a/examples/STM32_demo/docs/RTT_BIDIRECTIONAL_DESIGN.md +++ b/examples/STM32_demo/docs/RTT_BIDIRECTIONAL_DESIGN.md @@ -1,344 +1,55 @@ -# RTT Bidirectional Communication Implementation Design +# RTT Bidirectional Demo Design -## Overview +This document describes the RTT demo firmware shape used to exercise MCP RTT +read/write behavior. It is a design note for the example, not a release +certification record. -This document outlines the design and implementation of bidirectional RTT (Real-Time Transfer) communication for testing MCP embedded debugger functionality. The implementation will support both up channels (target→host) and down channels (host→target) to enable comprehensive testing of RTT read/write operations. +## Goals -## Current Status Analysis +- Expose multiple RTT up channels for terminal, data, and debug output. +- Expose RTT down channels for command and configuration input. +- Keep command parsing simple enough for manual and MCP-driven validation. +- Provide deterministic text output that can be captured in validation logs. -### Existing Implementation -- **Current demo**: Uses defmt-rtt for logging only (up channel) -- **Channels**: 1 up channel (defmt logging), 0 down channels -- **Target**: STM32G431CBTx (noted: Embed.toml shows STM32G031G8Ux - needs update) -- **MCP Server**: Supports RTT read/write but write currently fails due to missing down channels +## Channel Layout -### Limitations -- No bidirectional communication capability -- Cannot test MCP RTT write functionality -- Limited to logging-only use case +| Direction | Channel | Intended Use | +|-----------|---------|--------------| +| Up | 0 | Human-readable terminal output | +| Up | 1 | Periodic data stream | +| Up | 2 | Debug/status messages | +| Down | 0 | Short command input | +| Down | 1 | Configuration input | -## Design Goals +## Command Model -1. **Bidirectional Communication**: Enable both host→target and target→host data flow -2. **Multiple Channels**: Implement multiple up and down channels for testing -3. **Command Processing**: Simple command interface via down channels -4. **Data Streaming**: Multiple data types via up channels -5. **MCP Testing**: Comprehensive validation of all RTT MCP tools -6. **Real-time**: Maintain real-time characteristics without blocking +The demo accepts small ASCII commands for validation: -## Implementation Architecture +| Command | Purpose | +|---------|---------| +| `L` | Toggle LED state | +| `R` | Reset demo counters | +| `F` | Report current Fibonacci value | +| `I` | Print system information | +| `SPEED:n` | Adjust demo speed multiplier | +| `LED:ON` / `LED:OFF` | Set LED state | +| `MODE:AUTO` / `MODE:MANUAL` | Change calculation mode | +| `RESET` | Reset demo state | -### Channel Configuration +## Validation Expectations -```rust -// Target-side channel setup using rtt-target crate -let channels = rtt_init! { - up: { - 0: { - size: 1024, - mode: NoBlockSkip, - name: "Terminal" - } // General logging & status - 1: { - size: 512, - mode: NoBlockSkip, - name: "Data" - } // Fibonacci results & telemetry - 2: { - size: 256, - mode: NoBlockSkip, - name: "Debug" - } // Debug information - } - down: { - 0: { - size: 64, - mode: NoBlockSkip, - name: "Commands" - } // Single-character commands - 1: { - size: 128, - mode: NoBlockSkip, - name: "Config" - } // Configuration data - } -}; -``` +A validation run should prove: -### Channel Purpose +- RTT attach discovers the expected channels for the running firmware. +- Host reads receive text from up channels. +- Host writes reach down channels and trigger visible output. +- Invalid commands produce bounded error text rather than blocking the firmware. +- Detach and reconnect do not require restarting the host process. -| Channel | Direction | Purpose | Size | Format | -|---------|-----------|---------|------|--------| -| Up 0 | Target→Host | Terminal logging | 1024B | String | -| Up 1 | Target→Host | Fibonacci & telemetry | 512B | String | -| Up 2 | Target→Host | Debug information | 256B | String | -| Down 0 | Host→Target | Single-char commands | 64B | Raw bytes | -| Down 1 | Host→Target | Configuration data | 128B | Raw bytes | +## Limits -### Command Interface Design - -#### Simple Commands (Down Channel 0) -``` -'L' - Toggle LED -'R' - Reset Fibonacci counter -'S' - Start/Stop Fibonacci calculation -'F' - Request current Fibonacci value -'I' - Request system information -'P' - Pause for 5 seconds -'0'-'9' - Set calculation speed (0=fastest, 9=slowest) -``` - -#### Configuration Commands (Down Channel 1) -``` -"SPEED:n" - Set speed multiplier (n=1-10) -"LED:ON/OFF" - Control LED state -"MODE:AUTO/MANUAL" - Set calculation mode -"RESET" - Full system reset -``` - -## Implementation Plan - -### Phase 1: Dependencies and Setup -1. Add `rtt-target` dependency to Cargo.toml -2. Remove `defmt-rtt` dependency (replaced by rtt-target) -3. Update target chip in Embed.toml -4. Import necessary RTT modules - -### Phase 2: Core RTT Implementation -1. Initialize RTT with multiple channels -2. Replace defmt logging with RTT channel writes -3. Implement command reading infrastructure -4. Add command processing logic - -### Phase 3: Enhanced Features -1. Add bidirectional data flow -2. Implement various command types -3. Add telemetry and status reporting -4. Integrate with existing Fibonacci calculation - -### Phase 4: Testing and Validation -1. Test each RTT channel individually -2. Validate MCP RTT read operations -3. Validate MCP RTT write operations -4. Performance and stability testing - -## Detailed Implementation - -### Dependencies Update - -```toml -# Remove defmt-rtt, add rtt-target -[dependencies] -# defmt-rtt = { version = "1.0.0", optional = true } # Remove -rtt-target = "0.5" # Add - -# Remove defmt feature from default -[features] -default = ["panic-probe"] # Remove defmt-rtt -``` - -### Main Application Structure - -```rust -#![no_std] -#![no_main] - -use rtt_target::{rtt_init, rprintln}; -use core::fmt::Write; -use embassy_executor::Spawner; -use embassy_stm32::gpio::{Level, Output, Speed}; -use embassy_time::{Duration, Timer}; - -// Global state for commands -static mut LED_STATE: bool = false; -static mut CALCULATION_ACTIVE: bool = true; -static mut SPEED_MULTIPLIER: u32 = 1; - -#[embassy_executor::main] -async fn main(_spawner: Spawner) { - let p = embassy_stm32::init(Default::default()); - let mut led = Output::new(p.PB7, Level::Low, Speed::Low); - - // Initialize RTT with bidirectional channels - let channels = rtt_init! { - up: { - 0: { size: 1024, mode: NoBlockSkip, name: "Terminal" } - 1: { size: 512, mode: NoBlockSkip, name: "Data" } - 2: { size: 256, mode: NoBlockSkip, name: "Debug" } - } - down: { - 0: { size: 64, mode: NoBlockSkip, name: "Commands" } - 1: { size: 128, mode: NoBlockSkip, name: "Config" } - } - }; - - writeln!(channels.up.0, "RTT Bidirectional Demo Started").ok(); - writeln!(channels.up.2, "Channels: 3 Up, 2 Down").ok(); - - let mut fib_index = 0u32; - let mut cmd_buffer = [0u8; 64]; - let mut config_buffer = [0u8; 128]; - - loop { - // Process incoming commands - process_commands(&channels, &mut cmd_buffer, &mut config_buffer, &mut led).await; - - // Fibonacci calculation (if active) - if unsafe { CALCULATION_ACTIVE } { - let fib_value = fibonacci(fib_index); - writeln!(channels.up.1, "F({}) = {}", fib_index, fib_value).ok(); - - if fib_index % 10 == 0 { - writeln!(channels.up.0, "Milestone: {} Fibonacci numbers calculated", fib_index).ok(); - } - - fib_index += 1; - } - - // LED control - if unsafe { LED_STATE } { - led.set_high(); - } else { - led.set_low(); - } - - // Variable delay based on speed setting - let delay_ms = 1000 * unsafe { SPEED_MULTIPLIER }; - Timer::after(Duration::from_millis(delay_ms.into())).await; - } -} -``` - -### Command Processing Implementation - -```rust -async fn process_commands( - channels: &rtt_target::Channels, - cmd_buf: &mut [u8], - config_buf: &mut [u8], - led: &mut Output<'_> -) { - // Process single-character commands - if let Ok(bytes) = channels.down.0.read(cmd_buf) { - for i in 0..bytes { - match cmd_buf[i] as char { - 'L' => { - unsafe { LED_STATE = !LED_STATE; } - writeln!(channels.up.0, "LED toggled: {}", unsafe { LED_STATE }).ok(); - } - 'R' => { - // Reset handled in main loop - writeln!(channels.up.0, "Fibonacci counter reset requested").ok(); - } - 'S' => { - unsafe { CALCULATION_ACTIVE = !CALCULATION_ACTIVE; } - writeln!(channels.up.0, "Calculation {}", - if unsafe { CALCULATION_ACTIVE } { "started" } else { "stopped" }).ok(); - } - 'F' => { - writeln!(channels.up.0, "Current Fibonacci index: {}", /* current index */).ok(); - } - 'I' => { - writeln!(channels.up.0, "System: STM32G431CBTx, RTT Bidirectional Demo").ok(); - writeln!(channels.up.2, "Speed: {}, LED: {}, Calc: {}", - unsafe { SPEED_MULTIPLIER }, - unsafe { LED_STATE }, - unsafe { CALCULATION_ACTIVE }).ok(); - } - 'P' => { - writeln!(channels.up.0, "Pausing for 5 seconds...").ok(); - Timer::after(Duration::from_secs(5)).await; - writeln!(channels.up.0, "Pause complete").ok(); - } - '0'..='9' => { - let speed = (cmd_buf[i] - b'0' + 1) as u32; - unsafe { SPEED_MULTIPLIER = speed; } - writeln!(channels.up.0, "Speed set to: {}", speed).ok(); - } - _ => { - writeln!(channels.up.2, "Unknown command: 0x{:02X}", cmd_buf[i]).ok(); - } - } - } - } - - // Process configuration commands - if let Ok(bytes) = channels.down.1.read(config_buf) { - if bytes > 0 { - if let Ok(s) = core::str::from_utf8(&config_buf[..bytes]) { - writeln!(channels.up.0, "Config received: {}", s).ok(); - // Parse and process configuration - process_config_string(s, channels); - } - } - } -} -``` - -## Testing Strategy - -### MCP Tool Testing Sequence - -1. **RTT Attachment**: Verify all channels are detected -2. **Channel Discovery**: Confirm 3 up, 2 down channels -3. **Up Channel Reading**: Test reading from channels 0, 1, 2 -4. **Down Channel Writing**: Test writing commands to channels 0, 1 -5. **Command Processing**: Verify commands are executed -6. **Bidirectional Flow**: Test simultaneous read/write operations - -### Test Commands for MCP Validation - -```bash -# Test single commands -mcp__embedded-debugger__rtt_write --channel=0 --data="L" # Toggle LED -mcp__embedded-debugger__rtt_write --channel=0 --data="I" # System info -mcp__embedded-debugger__rtt_write --channel=0 --data="5" # Set speed - -# Test configuration commands -mcp__embedded-debugger__rtt_write --channel=1 --data="SPEED:3" -mcp__embedded-debugger__rtt_write --channel=1 --data="LED:ON" - -# Read responses -mcp__embedded-debugger__rtt_read --channel=0 # Terminal -mcp__embedded-debugger__rtt_read --channel=1 # Data -mcp__embedded-debugger__rtt_read --channel=2 # Debug -``` - -## Performance Considerations - -### Buffer Sizing -- **Up channels**: Larger buffers for continuous data flow -- **Down channels**: Smaller buffers for command efficiency -- **Total memory**: ~2KB RTT buffer usage - -### Real-time Characteristics -- Non-blocking RTT operations -- Command processing in main loop -- No interrupt-based RTT handling -- Graceful degradation under load - -## Success Criteria - -1. **✅ RTT Initialization**: 3 up + 2 down channels detected -2. **✅ MCP Read Operations**: All up channels readable via MCP -3. **✅ MCP Write Operations**: All down channels writable via MCP -4. **✅ Command Processing**: Commands executed correctly -5. **✅ Bidirectional Flow**: Simultaneous read/write operations -6. **✅ Stability**: Continuous operation without errors -7. **✅ Performance**: Real-time response to commands - -## Implementation Files - -### Modified Files -- `Cargo.toml` - Update dependencies -- `Embed.toml` - Correct target chip -- `src/main.rs` - Complete RTT bidirectional implementation -- `src/fmt.rs` - Update logging interface - -### New Files -- `RTT_BIDIRECTIONAL_DESIGN.md` - This design document -- `RTT_TESTING_GUIDE.md` - MCP testing procedures - ---- - -**Next Steps**: Begin implementation with Phase 1 (Dependencies and Setup), then proceed through each phase with testing validation at each step. \ No newline at end of file +- Channel counts depend on the flashed firmware image. +- RTT control block discovery depends on target state and memory map. +- Hardware validation must record fresh command output for the specific board. +- The CI job checks that the demo firmware compiles; it does not require a + connected target. diff --git a/examples/STM32_demo/docs/RTT_TESTING_RESULTS.md b/examples/STM32_demo/docs/RTT_TESTING_RESULTS.md index a429bb8..c036dae 100644 --- a/examples/STM32_demo/docs/RTT_TESTING_RESULTS.md +++ b/examples/STM32_demo/docs/RTT_TESTING_RESULTS.md @@ -1,392 +1,60 @@ -# RTT Bidirectional Communication - Testing Results - -## Overview - -This document provides comprehensive testing results for the RTT bidirectional communication implementation. The testing validates both up channels (target→host) and down channels (host→target) functionality using the MCP embedded debugger server. - -## Test Environment - -- **Target**: STM32G431CBTx microcontroller -- **Debug Probe**: ST-Link V2 (VID:PID = 0483:3748) -- **Firmware**: demo_code - RTT Bidirectional Demo with Fibonacci calculation -- **MCP Server**: embedded-debugger-mcp v0.1.0 with RTT optimization -- **Test Date**: 2025-08-05 - -## Implementation Summary - -### Channel Configuration - -| Channel | Direction | Name | Size | Purpose | -|---------|-----------|------|------|---------| -| Up 0 | Target→Host | Terminal | 1024B | General logging & command responses | -| Up 1 | Target→Host | Data | 512B | Fibonacci calculation results | -| Up 2 | Target→Host | Debug | 256B | System status & debug information | -| Down 0 | Host→Target | Commands | 64B | Single-character commands | -| Down 1 | Host→Target | Config | 128B | Configuration strings | - -### Deployment Results - -``` -🚀 Firmware deployment completed! - -Session ID: session_1754377337779 -File: G:\codes\MCP_server\demo_code\target\thumbv7em-none-eabi\release\demo_code -Format: elf -Total Time: 6.1s - -Status: -🔄 Step 1/5: Erasing flash memory... -✅ Flash erased successfully -🔄 Step 2/5: Programming firmware... -✅ Programmed 510636 bytes -🔄 Step 3/5: Resetting target... -✅ Target reset successfully -✅ Target running -🔄 Step 4/5: Attaching RTT (probe-rs style)... -✅ RTT attached on attempt 1 (3 up, 2 down channels) -🔄 Step 5/5: Finalizing... - -✅ Firmware is now running on target. -``` - -**Result: ✅ Perfect deployment with RTT attachment on first attempt** - -## Channel Discovery Testing - -### RTT Channel Detection - -``` -📋 RTT Channels - -Session ID: session_1754377337779 - -📥 Up Channels (Target → Host): - 0. Terminal (Size: 1024 bytes, Mode: RTT) - 2. Debug (Size: 256 bytes, Mode: RTT) - 1. Data (Size: 512 bytes, Mode: RTT) - -📤 Down Channels (Host → Target): - 0. Commands (Size: 64 bytes, Mode: RTT) - 1. Config (Size: 128 bytes, Mode: RTT) -``` - -**Result: ✅ All 5 channels detected correctly (3 up + 2 down)** - -## Up Channel Testing (Target → Host) - -### Terminal Channel (Up 0) - System Messages - -``` -📥 RTT Read from Channel 0 - -Bytes Read: 516 - -Data: -Text: === RTT Bidirectional Communication Demo === -Target: STM32G431CBTx -Channels: 3 Up (Terminal/Data/Debug), 2 Down (Commands/Config) - -Available Commands (send to down channel 0): - L - Toggle LED - R - Reset Fibonacci counter - S - Start/Stop calculation - F - Get current Fibonacci value - I - System information - P - Pause 5 seconds - 0-9 - Set speed (1x-10x) - -Config Commands (send to down channel 1): - SPEED:n, LED:ON/OFF, MODE:AUTO/MANUAL, RESET - -Starting main loop... -Milestone: 10 Fibonacci numbers calculated -``` - -**Result: ✅ Terminal channel displaying startup messages and help text** - -### Data Channel (Up 1) - Fibonacci Results - -``` -📥 RTT Read from Channel 1 - -Bytes Read: 255 - -Data: -Text: F(0) = 0 -F(1) = 1 -F(2) = 1 -F(3) = 2 -F(4) = 3 -F(5) = 5 -F(6) = 8 -F(7) = 13 -F(8) = 21 -F(9) = 34 -F(10) = 55 -F(11) = 89 -F(12) = 144 -F(13) = 233 -F(14) = 377 -F(15) = 610 -F(16) = 987 -F(17) = 1597 -F(18) = 2584 -F(19) = 4181 -F(20) = 6765 -F(21) = 10946 -F(22) = 17711 -``` - -**Result: ✅ Data channel streaming Fibonacci calculations correctly** - -### Debug Channel (Up 2) - System Status - -``` -📥 RTT Read from Channel 2 - -Bytes Read: 106 - -Data: -Text: RTT initialization complete -Status - Speed:1, LED:true, Calc:true, Index:43 -Status: Loop#50, Speed:1x, LED:true, Calc:true, FibIdx:50 -``` - -**Result: ✅ Debug channel showing system status and periodic reports** - -## Down Channel Testing (Host → Target) - -### Command Channel (Down 0) - Single Commands - -#### Test 1: LED Toggle Command - -**Command Sent:** -``` -📤 RTT Write to Channel 0 -Data: L -Bytes Written: 1 -``` - -**Response Received:** -``` -📥 RTT Read from Channel 0 -Data: LED toggled: true -``` - -**Result: ✅ LED toggle command processed successfully** - -#### Test 2: System Information Command - -**Command Sent:** -``` -📤 RTT Write to Channel 0 -Data: I -Bytes Written: 1 -``` - -**Response Received:** -``` -📥 RTT Read from Channel 0 -Data: System: STM32G431CBTx RTT Bidirectional Demo - -📥 RTT Read from Channel 2 (Debug) -Data: Status - Speed:1, LED:true, Calc:true, Index:43 -``` - -**Result: ✅ System information command working with multi-channel response** - -#### Test 3: Reset Command - -**Command Sent:** -``` -📤 RTT Write to Channel 0 -Data: R -Bytes Written: 1 -``` - -**Response Received:** -``` -📥 RTT Read from Channel 0 -Data: Fibonacci counter reset to 0 -Milestone: 10 Fibonacci numbers calculated - -📥 RTT Read from Channel 1 (Data) -Data: F(0) = 0 -F(1) = 1 -F(2) = 1 -... -``` - -**Result: ✅ Reset command successfully restarted Fibonacci calculation** - -### Configuration Channel (Down 1) - Multi-byte Commands - -#### Test 1: Speed Configuration - -**Command Sent:** -``` -📤 RTT Write to Channel 1 -Data: SPEED:3 -Bytes Written: 7 -``` - -**Response Received:** -``` -📥 RTT Read from Channel 0 -Data: Config received: SPEED:3 -Speed configured to: 3x -``` - -**Result: ✅ Configuration parsing and application successful** - -## Performance Analysis - -### RTT Connection Performance -- **Attachment Time**: <100ms (first attempt success) -- **Channel Discovery**: All 5 channels detected immediately -- **Write Latency**: <10ms for command processing -- **Read Throughput**: Continuous data streaming without loss - -### Data Integrity -- **Command Processing**: 100% success rate -- **Data Streaming**: No data corruption observed -- **Multi-channel**: Simultaneous read/write operations successful -- **UTF-8 Encoding**: Proper string handling in both directions - -### System Resources -- **Memory Usage**: ~2KB total RTT buffer space -- **CPU Impact**: Minimal, non-blocking operations -- **Real-time Performance**: Commands processed immediately - -## Command Interface Validation - -### Single-Character Commands (Down Channel 0) - -| Command | Function | Test Result | -|---------|----------|-------------| -| L | Toggle LED | ✅ Working | -| R | Reset Fibonacci counter | ✅ Working | -| S | Start/Stop calculation | ✅ Working | -| F | Get current Fibonacci value | ✅ Working | -| I | System information | ✅ Working | -| P | Pause 5 seconds | ✅ Working | -| 0-9 | Set speed (1x-10x) | ✅ Working | - -### Configuration Commands (Down Channel 1) - -| Command | Function | Test Result | -|---------|----------|-------------| -| SPEED:n | Set speed multiplier | ✅ Working | -| LED:ON/OFF | Control LED state | ✅ Working | -| MODE:AUTO/MANUAL | Set calculation mode | ✅ Working | -| RESET | Full system reset | ✅ Working | - -## MCP Tool Validation - -### RTT Read Operations - -| Tool | Channel | Result | Data Type | -|------|---------|---------|-----------| -| rtt_read | Up 0 (Terminal) | ✅ Success | Text messages | -| rtt_read | Up 1 (Data) | ✅ Success | Fibonacci results | -| rtt_read | Up 2 (Debug) | ✅ Success | Status information | - -### RTT Write Operations - -| Tool | Channel | Result | Response | -|------|---------|---------|----------| -| rtt_write | Down 0 (Commands) | ✅ Success | Immediate command execution | -| rtt_write | Down 1 (Config) | ✅ Success | Configuration applied | - -### Channel Management - -| Tool | Function | Result | -|------|----------|---------| -| rtt_channels | Channel discovery | ✅ All 5 channels detected | -| rtt_attach | Connection establishment | ✅ First attempt success | -| rtt_detach | Clean disconnection | ✅ Working | - -## Real-world Usage Scenarios - -### Scenario 1: Interactive Debugging -- **Use Case**: Send commands during firmware execution -- **Test**: LED control, speed adjustment, system queries -- **Result**: ✅ Real-time interaction successful - -### Scenario 2: Data Monitoring -- **Use Case**: Continuous data streaming with control -- **Test**: Fibonacci calculation with reset/pause commands -- **Result**: ✅ Uninterrupted data flow with command injection - -### Scenario 3: Configuration Management -- **Use Case**: Runtime parameter adjustment -- **Test**: Speed changes, mode switching via config channel -- **Result**: ✅ Configuration updates applied immediately - -## Key Achievements - -### 1. Complete Bidirectional Communication -- ✅ **3 Up Channels**: Terminal, Data, Debug -- ✅ **2 Down Channels**: Commands, Config -- ✅ **Real-time Processing**: Commands executed immediately -- ✅ **Multi-channel Coordination**: Responses across different channels - -### 2. MCP Integration Excellence -- ✅ **100% MCP Compatibility**: All RTT tools working -- ✅ **First-attempt Connection**: RTT attachment reliable -- ✅ **Channel Discovery**: Automatic detection of all channels -- ✅ **Error-free Operation**: No communication failures - -### 3. Production-Ready Implementation -- ✅ **Robust Error Handling**: Invalid commands handled gracefully -- ✅ **UTF-8 Support**: Proper string encoding/decoding -- ✅ **Performance**: Real-time response without blocking -- ✅ **Extensible Design**: Easy to add new commands/channels - -### 4. Technical Validation -- ✅ **API Compatibility**: rtt-target v0.5.0 fully supported -- ✅ **Memory Efficiency**: Optimal buffer sizing -- ✅ **Hardware Integration**: STM32G431CBTx validated -- ✅ **Probe Support**: ST-Link V2 verified - -## Comparison with Previous Implementation - -| Aspect | Previous (defmt-only) | New (RTT Bidirectional) | -|--------|----------------------|--------------------------| -| **Channels** | 1 up, 0 down | 3 up, 2 down | -| **Communication** | One-way logging | Full bidirectional | -| **MCP RTT Write** | ❌ Failed | ✅ Working | -| **Command Processing** | ❌ None | ✅ Full interface | -| **Data Streaming** | Basic logging | Structured multi-channel | -| **Real-time Control** | ❌ Not possible | ✅ Interactive debugging | - -## Conclusion - -### ✅ Complete Success - -The RTT bidirectional communication implementation has achieved **100% success** across all testing categories: - -1. **Channel Implementation**: All 5 channels working perfectly -2. **MCP Integration**: Full compatibility with embedded debugger tools -3. **Bidirectional Flow**: Commands and responses working flawlessly -4. **Real-time Performance**: Immediate command processing -5. **Data Integrity**: No corruption or data loss observed -6. **Production Readiness**: Stable, robust, and extensible - -### 🚀 Ready for Production Use - -This implementation provides: -- **Complete RTT Testing Platform** for MCP validation -- **Interactive Debugging Interface** for embedded development -- **Real-time Data Streaming** with bidirectional control -- **Extensible Command Framework** for custom applications - -### 📊 Final Assessment - -**Status: ✅ FULLY FUNCTIONAL AND PRODUCTION READY** - -The RTT bidirectional implementation successfully transforms the MCP embedded debugger from a basic RTT reader into a full bidirectional communication platform, enabling real-time interactive debugging and control of embedded systems. - ---- - -*Testing completed on 2025-08-05 using MCP embedded debugger with STM32G431CBTx target* \ No newline at end of file +# RTT Hardware Validation Notes + +This file summarizes a historical RTT validation pass for the STM32 demo. It +should be refreshed when validating a release candidate or a new board. + +## Environment + +- Target used: STM32G431CBTx +- Probe used: ST-Link V2 +- Firmware: `examples/STM32_demo` +- Host: embedded-debugger-mcp MCP server + +## Validation Areas + +| Area | Expected Evidence | +|------|-------------------| +| Firmware deployment | Flash command output, reset output, and target state after reset | +| RTT attach | Channel counts and attach result | +| RTT read | Captured text from terminal, data, and debug channels | +| RTT write | Command output that proves down-channel input reached firmware | +| Configuration channel | Output showing configuration input was parsed | +| Detach/reconnect | Clean detach followed by successful attach | + +## Commands Exercised + +| Command | Purpose | +|---------|---------| +| `L` | Toggle LED state | +| `R` | Reset demo counters | +| `F` | Report current Fibonacci value | +| `I` | Print system information | +| `SPEED:n` | Set speed multiplier | +| `LED:ON` / `LED:OFF` | Control LED state | +| `MODE:AUTO` / `MODE:MANUAL` | Set calculation mode | +| `RESET` | Reset demo state | + +## Release Interpretation + +- Treat these notes as example validation history only. +- Do not claim hardware success for a new release without fresh output. +- Record rejected destructive operations as evidence when testing safety gates. +- CI checks compileability of the firmware example, not live target behavior. + +## Suggested Fresh Evidence + +For future validation, capture command output for: + +```text +list_probes +connect +flash_program +rtt_attach +rtt_channels +rtt_read +rtt_write +rtt_detach +disconnect +``` + +Include the config file and target identifier with the run record. diff --git a/src/config.rs b/src/config.rs index 0640883..cbcb4d7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,40 +17,40 @@ pub struct Args { pub config: Option, /// Log level (error, warn, info, debug, trace) - #[arg(long, default_value = "info")] - pub log_level: String, + #[arg(long)] + pub log_level: Option, /// Log file path #[arg(long)] pub log_file: Option, /// Maximum number of concurrent debug sessions - #[arg(long, default_value = "5")] - pub max_sessions: usize, + #[arg(long)] + pub max_sessions: Option, /// Session timeout in seconds - #[arg(long, default_value = "3600")] - pub session_timeout: u64, + #[arg(long)] + pub session_timeout: Option, /// Default debugger speed in kHz - #[arg(long, default_value = "4000")] - pub default_speed: u32, + #[arg(long)] + pub default_speed: Option, /// Connection timeout in milliseconds - #[arg(long, default_value = "5000")] - pub connection_timeout: u64, + #[arg(long)] + pub connection_timeout: Option, /// Connection retry count - #[arg(long, default_value = "3")] - pub retry_count: u32, + #[arg(long)] + pub retry_count: Option, /// RTT buffer size in bytes - #[arg(long, default_value = "1024")] - pub rtt_buffer_size: usize, + #[arg(long)] + pub rtt_buffer_size: Option, /// RTT poll interval in milliseconds - #[arg(long, default_value = "10")] - pub rtt_poll_interval: u64, + #[arg(long)] + pub rtt_poll_interval: Option, /// Allow flash erase operations #[arg(long)] @@ -180,17 +180,39 @@ impl Config { /// Merge command line arguments into configuration pub fn merge_args(&mut self, args: &Args) { - self.server.max_sessions = args.max_sessions; - self.server.session_timeout_seconds = args.session_timeout; - self.debugger.default_speed_khz = args.default_speed; - self.debugger.connection_timeout_ms = args.connection_timeout; - self.debugger.retry_count = args.retry_count; - self.rtt.buffer_size = args.rtt_buffer_size; - self.rtt.poll_interval_ms = args.rtt_poll_interval; - self.security.allow_flash_erase = args.allow_flash_erase; - self.security.restrict_memory_access = args.restrict_memory_access; - self.logging.level = args.log_level.clone(); - self.logging.file = args.log_file.clone(); + if let Some(max_sessions) = args.max_sessions { + self.server.max_sessions = max_sessions; + } + if let Some(session_timeout) = args.session_timeout { + self.server.session_timeout_seconds = session_timeout; + } + if let Some(default_speed) = args.default_speed { + self.debugger.default_speed_khz = default_speed; + } + if let Some(connection_timeout) = args.connection_timeout { + self.debugger.connection_timeout_ms = connection_timeout; + } + if let Some(retry_count) = args.retry_count { + self.debugger.retry_count = retry_count; + } + if let Some(rtt_buffer_size) = args.rtt_buffer_size { + self.rtt.buffer_size = rtt_buffer_size; + } + if let Some(rtt_poll_interval) = args.rtt_poll_interval { + self.rtt.poll_interval_ms = rtt_poll_interval; + } + if args.allow_flash_erase { + self.security.allow_flash_erase = true; + } + if args.restrict_memory_access { + self.security.restrict_memory_access = true; + } + if let Some(log_level) = &args.log_level { + self.logging.level = log_level.clone(); + } + if let Some(log_file) = &args.log_file { + self.logging.file = Some(log_file.clone()); + } } /// Validate configuration diff --git a/src/main.rs b/src/main.rs index ab6f0bc..6d8c75f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,20 +35,8 @@ async fn main() -> Result<(), Box> { return Ok(()); } - // Initialize logging - init_logging(&args)?; - - info!( - "Starting Debugger MCP Server v{}", - env!("CARGO_PKG_VERSION") - ); - debug!("Command line args: {:?}", args); - // Load configuration - let mut config = Config::load(args.config.as_ref()).map_err(|e| { - error!("Failed to load configuration: {}", e); - e - })?; + let mut config = Config::load(args.config.as_ref())?; // Merge command line arguments into configuration config.merge_args(&args); @@ -65,10 +53,16 @@ async fn main() -> Result<(), Box> { } // Validate final configuration - config.validate().map_err(|e| { - error!("Configuration validation failed: {}", e); - e - })?; + config.validate()?; + + // Initialize logging after config merge so file config is not overwritten by CLI defaults. + init_logging(&config)?; + + info!( + "Starting Debugger MCP Server v{}", + env!("CARGO_PKG_VERSION") + ); + debug!("Command line args: {:?}", args); info!("Configuration loaded and validated successfully"); @@ -206,9 +200,9 @@ fn command_output(command: &str, args: &[&str]) -> Option { } /// Initialize logging system -fn init_logging(args: &Args) -> Result<(), Box> { +fn init_logging(config: &Config) -> Result<(), Box> { let env_filter = - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)); + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.logging.level)); let subscriber = fmt::Subscriber::builder() .with_env_filter(env_filter) @@ -218,7 +212,7 @@ fn init_logging(args: &Args) -> Result<(), Box> { .with_line_number(false); // Configure output destination - if let Some(log_file) = &args.log_file { + if let Some(log_file) = &config.logging.file { let file = std::fs::OpenOptions::new() .create(true) .append(true) @@ -226,12 +220,12 @@ fn init_logging(args: &Args) -> Result<(), Box> { subscriber.with_writer(file).init(); - println!("Logging to file: {}", log_file.display()); + eprintln!("Logging to file: {}", log_file.display()); } else { subscriber.with_writer(std::io::stderr).init(); } - debug!("Logging initialized with level: {}", args.log_level); + debug!("Logging initialized with level: {}", config.logging.level); Ok(()) } @@ -249,8 +243,8 @@ mod tests { "10", ]); - assert_eq!(args.log_level, "debug"); - assert_eq!(args.max_sessions, 10); + assert_eq!(args.log_level.as_deref(), Some("debug")); + assert_eq!(args.max_sessions, Some(10)); assert!(args.command.is_none()); } diff --git a/src/tools/debugger_tools.rs b/src/tools/debugger_tools.rs index fa9c31d..f9b12b4 100644 --- a/src/tools/debugger_tools.rs +++ b/src/tools/debugger_tools.rs @@ -20,6 +20,10 @@ use super::types::*; use crate::config::{Config, TargetConfig}; use crate::rtt::RttManager; +const RTT_CONTROL_BLOCK_HEADER_BYTES: usize = 16; +type RttScanRange = (u64, u64); +type PreparedRttScan = (Option, Option>); + // Probe-rs imports use probe_rs::probe::list::Lister; use probe_rs::{CoreStatus, MemoryInterface, Permissions, RegisterValue, Session}; @@ -148,6 +152,21 @@ impl EmbeddedDebuggerToolHandler { address: u64, size: usize, required_access: char, + ) -> Result<(), McpError> { + self.ensure_memory_region_allowed_for_target( + &session.target_chip, + address, + size, + required_access, + ) + } + + fn ensure_memory_region_allowed_for_target( + &self, + target_chip: &str, + address: u64, + size: usize, + required_access: char, ) -> Result<(), McpError> { let end_exclusive = address.checked_add(size as u64).ok_or_else(|| { McpError::internal_error( @@ -160,17 +179,15 @@ impl EmbeddedDebuggerToolHandler { return Ok(()); } - let target = self - .target_config_for(&session.target_chip) - .ok_or_else(|| { - McpError::internal_error( - format!( + let target = self.target_config_for(target_chip).ok_or_else(|| { + McpError::internal_error( + format!( "Memory access is restricted, but target '{}' has no configured memory map.", - session.target_chip + target_chip ), - None, - ) - })?; + None, + ) + })?; let last_address = end_exclusive - 1; let allowed = target.memory_regions.iter().any(|region| { @@ -185,13 +202,125 @@ impl EmbeddedDebuggerToolHandler { Err(McpError::internal_error( format!( "Memory range 0x{address:08X}..0x{last_address:08X} is outside configured '{}' access regions for target '{}'.", - required_access, session.target_chip + required_access, target_chip ), None, )) } } + fn prepare_rtt_scan_region( + &self, + target_chip: &str, + control_block_address: Option, + memory_ranges: Option>, + ) -> Result { + let control_block_address = control_block_address.or(self.config.rtt.control_block_address); + + if let Some(address) = control_block_address { + self.ensure_memory_region_allowed_for_target( + target_chip, + address, + RTT_CONTROL_BLOCK_HEADER_BYTES, + 'r', + )?; + return Ok((Some(address), None)); + } + + if let Some(ranges) = memory_ranges { + self.ensure_rtt_ranges_allowed(target_chip, &ranges)?; + return Ok((None, Some(ranges))); + } + + if self.config.security.restrict_memory_access { + return Ok((None, Some(self.configured_rtt_scan_ranges(target_chip)?))); + } + + Ok((None, None)) + } + + fn ensure_rtt_ranges_allowed( + &self, + target_chip: &str, + ranges: &[RttScanRange], + ) -> Result<(), McpError> { + if ranges.is_empty() { + return Err(McpError::internal_error( + "RTT memory ranges must not be empty.".to_string(), + None, + )); + } + + for (start, end) in ranges { + let size = end.checked_sub(*start).ok_or_else(|| { + McpError::internal_error( + format!( + "RTT memory range 0x{start:08X}..0x{end:08X} has an invalid end address." + ), + None, + ) + })?; + if size == 0 { + return Err(McpError::internal_error( + format!("RTT memory range 0x{start:08X}..0x{end:08X} is empty."), + None, + )); + } + let size = usize::try_from(size).map_err(|_| { + McpError::internal_error( + format!("RTT memory range 0x{start:08X}..0x{end:08X} is too large."), + None, + ) + })?; + self.ensure_memory_region_allowed_for_target(target_chip, *start, size, 'r')?; + } + + Ok(()) + } + + fn configured_rtt_scan_ranges(&self, target_chip: &str) -> Result, McpError> { + let target = self.target_config_for(target_chip).ok_or_else(|| { + McpError::internal_error( + format!( + "RTT scan is restricted, but target '{}' has no configured memory map.", + target_chip + ), + None, + ) + })?; + + let ranges: Result, _> = target + .memory_regions + .iter() + .filter(|region| region.access.contains('r')) + .filter(|region| { + !self.config.rtt.scan_ram_only || region.name.to_ascii_lowercase().contains("ram") + }) + .map(|region| { + let end_exclusive = region.end.checked_add(1).ok_or_else(|| { + McpError::internal_error( + format!("Memory region '{}' end address overflows.", region.name), + None, + ) + })?; + Ok((region.start, end_exclusive)) + }) + .collect(); + + let ranges = ranges?; + if ranges.is_empty() { + return Err(McpError::internal_error( + format!( + "RTT scan is restricted, but target '{}' has no readable configured ranges.", + target_chip + ), + None, + )); + } + + Ok(ranges) + } + fn target_config_for(&self, target_chip: &str) -> Option<&TargetConfig> { let target_chip_lower = target_chip.to_lowercase(); self.config @@ -1313,6 +1442,12 @@ impl EmbeddedDebuggerToolHandler { None }; + let (control_block_address, memory_ranges) = self.prepare_rtt_scan_region( + &session_arc.target_chip, + control_block_address, + memory_ranges, + )?; + // Attach RTT { let mut rtt_manager = session_arc.rtt_manager.lock().await; @@ -1790,6 +1925,8 @@ impl EmbeddedDebuggerToolHandler { args.session_id, args.file_path ); + self.ensure_flash_erase_allowed()?; + let session_arc = self.get_session(&args.session_id).await?; // Parse file path and format @@ -2483,3 +2620,68 @@ impl ServerHandler for EmbeddedDebuggerToolHandler { Ok(self.get_info()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn flash_program_rejects_when_erase_permission_disabled() { + let handler = EmbeddedDebuggerToolHandler::new(Config::default()); + let result = handler + .flash_program(Parameters(FlashProgramArgs { + session_id: "missing-session".to_string(), + file_path: "firmware.elf".to_string(), + format: "auto".to_string(), + base_address: None, + verify: true, + })) + .await; + + let error = format!("{:?}", result.expect_err("flash program must fail closed")); + assert!(error.contains("Flash erase is disabled")); + } + + #[test] + fn restricted_rtt_exact_address_uses_target_memory_map() { + let mut config = Config::default(); + config.security.restrict_memory_access = true; + let handler = EmbeddedDebuggerToolHandler::new(config); + + assert!(handler + .prepare_rtt_scan_region("STM32F407VGTx", Some(0x2000_0000), None) + .is_ok()); + assert!(handler + .prepare_rtt_scan_region("STM32F407VGTx", Some(0x4000_0000), None) + .is_err()); + } + + #[test] + fn restricted_rtt_ranges_must_stay_inside_readable_regions() { + let mut config = Config::default(); + config.security.restrict_memory_access = true; + let handler = EmbeddedDebuggerToolHandler::new(config); + + assert!(handler + .prepare_rtt_scan_region( + "STM32F407VGTx", + None, + Some(vec![(0x2000_0000, 0x2000_0100)]) + ) + .is_ok()); + assert!(handler + .prepare_rtt_scan_region( + "STM32F407VGTx", + None, + Some(vec![(0x2002_FF00, 0x2003_0100)]) + ) + .is_err()); + assert!(handler + .prepare_rtt_scan_region( + "STM32F407VGTx", + None, + Some(vec![(0x2000_0000, 0x2000_0000)]) + ) + .is_err()); + } +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index cfa4e34..2c7b28f 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,5 +1,7 @@ //! Integration tests for debugger MCP server +use clap::Parser; +use embedded_debugger_mcp::config::Args; use embedded_debugger_mcp::Config; #[tokio::test] @@ -27,6 +29,44 @@ async fn test_probe_discovery() { println!("Found {} probes", probes.len()); } +#[test] +fn test_cli_defaults_do_not_override_config_file_values() { + let mut config = Config::default(); + config.server.max_sessions = 9; + config.debugger.default_speed_khz = 1200; + config.security.allow_flash_erase = true; + config.security.restrict_memory_access = true; + + let args = Args::parse_from(["embedded-debugger-mcp"]); + config.merge_args(&args); + + assert_eq!(config.server.max_sessions, 9); + assert_eq!(config.debugger.default_speed_khz, 1200); + assert!(config.security.allow_flash_erase); + assert!(config.security.restrict_memory_access); +} + +#[test] +fn test_cli_explicit_values_override_config_file_values() { + let mut config = Config::default(); + let args = Args::parse_from([ + "embedded-debugger-mcp", + "--max-sessions", + "10", + "--default-speed", + "1600", + "--allow-flash-erase", + "--restrict-memory-access", + ]); + + config.merge_args(&args); + + assert_eq!(config.server.max_sessions, 10); + assert_eq!(config.debugger.default_speed_khz, 1600); + assert!(config.security.allow_flash_erase); + assert!(config.security.restrict_memory_access); +} + #[test] fn test_error_types() { use embedded_debugger_mcp::DebugError; From af7b9c3102f67b427d88deb755886eda771e77b7 Mon Sep 17 00:00:00 2001 From: Hongda Chen <54146249+Adancurusul@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:22:25 +0800 Subject: [PATCH 3/8] refactor: split debugger tool handler modules --- src/tools/debugger_tools.rs | 2700 +------------------- src/tools/debugger_tools/flash.rs | 652 +++++ src/tools/debugger_tools/formatting.rs | 157 ++ src/tools/debugger_tools/guards.rs | 391 +++ src/tools/debugger_tools/management.rs | 320 +++ src/tools/debugger_tools/memory.rs | 331 +++ src/tools/debugger_tools/rtt.rs | 439 ++++ src/tools/debugger_tools/server.rs | 28 + src/tools/debugger_tools/session.rs | 113 + src/tools/debugger_tools/target_control.rs | 366 +++ 10 files changed, 2811 insertions(+), 2686 deletions(-) create mode 100644 src/tools/debugger_tools/flash.rs create mode 100644 src/tools/debugger_tools/formatting.rs create mode 100644 src/tools/debugger_tools/guards.rs create mode 100644 src/tools/debugger_tools/management.rs create mode 100644 src/tools/debugger_tools/memory.rs create mode 100644 src/tools/debugger_tools/rtt.rs create mode 100644 src/tools/debugger_tools/server.rs create mode 100644 src/tools/debugger_tools/session.rs create mode 100644 src/tools/debugger_tools/target_control.rs diff --git a/src/tools/debugger_tools.rs b/src/tools/debugger_tools.rs index f9b12b4..31d5683 100644 --- a/src/tools/debugger_tools.rs +++ b/src/tools/debugger_tools.rs @@ -1,2687 +1,15 @@ -//! RMCP 0.3.2 implementation for embedded debugger MCP tools +//! RMCP tool handler implementation for embedded debugging. //! -//! This implementation provides all 18 debugging tools (13 base + 5 RTT) using real probe-rs integration - -use rmcp::{ - handler::server::{router::tool::ToolRouter, tool::Parameters}, - model::*, - service::RequestContext, - tool, tool_handler, tool_router, ErrorData as McpError, RoleServer, ServerHandler, -}; -use std::collections::HashMap; -use std::future::Future; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use tokio::sync::{OwnedSemaphorePermit, RwLock, Semaphore}; -use tracing::{debug, error, info, warn}; - -use super::types::*; -// Flash types will be used through crate::flash:: prefix -use crate::config::{Config, TargetConfig}; -use crate::rtt::RttManager; - -const RTT_CONTROL_BLOCK_HEADER_BYTES: usize = 16; -type RttScanRange = (u64, u64); -type PreparedRttScan = (Option, Option>); - -// Probe-rs imports -use probe_rs::probe::list::Lister; -use probe_rs::{CoreStatus, MemoryInterface, Permissions, RegisterValue, Session}; - -/// Debug session information -pub struct DebugSession { - pub session_id: String, - pub probe_identifier: String, - pub target_chip: String, - pub created_at: chrono::DateTime, - pub session: Arc>, - pub rtt_manager: Arc>, - _session_slot: OwnedSemaphorePermit, -} - -/// Embedded debugger tool handler with debug, RTT, and flash tools -#[derive(Clone)] -pub struct EmbeddedDebuggerToolHandler { - #[allow(dead_code)] - tool_router: ToolRouter, - sessions: Arc>>>, - config: Arc, - max_sessions: usize, - session_slots: Arc, -} - -impl EmbeddedDebuggerToolHandler { - pub fn new(config: impl Into) -> Self { - let config = config.into(); - let max_sessions = config.server.max_sessions; - Self { - tool_router: Self::tool_router(), - sessions: Arc::new(RwLock::new(HashMap::new())), - config: Arc::new(config), - max_sessions, - session_slots: Arc::new(Semaphore::new(max_sessions)), - } - } - - async fn get_session(&self, session_id: &str) -> Result, McpError> { - let sessions = self.sessions.read().await; - sessions.get(session_id).cloned().ok_or_else(|| { - McpError::internal_error( - format!( - "Session '{}' not found. Use 'connect' to establish a debug session first.", - session_id - ), - None, - ) - }) - } - - fn flash_erase_allowed(&self) -> bool { - self.config.security.allow_flash_erase || self.config.flash.allow_erase - } - - fn ensure_flash_erase_allowed(&self) -> Result<(), McpError> { - if self.flash_erase_allowed() { - Ok(()) - } else { - Err(McpError::internal_error( - "Flash erase is disabled by configuration. Enable security.allow_flash_erase or flash.allow_erase to use this operation." - .to_string(), - None, - )) - } - } - - fn ensure_memory_read_allowed( - &self, - session: &DebugSession, - address: u64, - size: usize, - ) -> Result<(), McpError> { - if size == 0 { - return Err(McpError::internal_error( - "Memory read size must be greater than zero.".to_string(), - None, - )); - } - if size > self.config.memory.max_read_size { - return Err(McpError::internal_error( - format!( - "Memory read size {} exceeds configured limit {}.", - size, self.config.memory.max_read_size - ), - None, - )); - } - self.ensure_memory_region_allowed(session, address, size, 'r') - } - - fn ensure_memory_write_allowed( - &self, - session: &DebugSession, - address: u64, - size: usize, - ) -> Result<(), McpError> { - if !self.config.security.allow_memory_write { - return Err(McpError::internal_error( - "Memory writes are disabled by configuration.".to_string(), - None, - )); - } - if size == 0 { - return Err(McpError::internal_error( - "Memory write size must be greater than zero.".to_string(), - None, - )); - } - if size > self.config.memory.max_write_size { - return Err(McpError::internal_error( - format!( - "Memory write size {} exceeds configured limit {}.", - size, self.config.memory.max_write_size - ), - None, - )); - } - self.ensure_memory_region_allowed(session, address, size, 'w') - } - - fn ensure_memory_region_allowed( - &self, - session: &DebugSession, - address: u64, - size: usize, - required_access: char, - ) -> Result<(), McpError> { - self.ensure_memory_region_allowed_for_target( - &session.target_chip, - address, - size, - required_access, - ) - } - - fn ensure_memory_region_allowed_for_target( - &self, - target_chip: &str, - address: u64, - size: usize, - required_access: char, - ) -> Result<(), McpError> { - let end_exclusive = address.checked_add(size as u64).ok_or_else(|| { - McpError::internal_error( - "Memory range overflows u64 address space.".to_string(), - None, - ) - })?; - - if !self.config.security.restrict_memory_access { - return Ok(()); - } - - let target = self.target_config_for(target_chip).ok_or_else(|| { - McpError::internal_error( - format!( - "Memory access is restricted, but target '{}' has no configured memory map.", - target_chip - ), - None, - ) - })?; - - let last_address = end_exclusive - 1; - let allowed = target.memory_regions.iter().any(|region| { - address >= region.start - && last_address <= region.end - && region.access.contains(required_access) - }); - - if allowed { - Ok(()) - } else { - Err(McpError::internal_error( - format!( - "Memory range 0x{address:08X}..0x{last_address:08X} is outside configured '{}' access regions for target '{}'.", - required_access, target_chip - ), - None, - )) - } - } - - fn prepare_rtt_scan_region( - &self, - target_chip: &str, - control_block_address: Option, - memory_ranges: Option>, - ) -> Result { - let control_block_address = control_block_address.or(self.config.rtt.control_block_address); - - if let Some(address) = control_block_address { - self.ensure_memory_region_allowed_for_target( - target_chip, - address, - RTT_CONTROL_BLOCK_HEADER_BYTES, - 'r', - )?; - return Ok((Some(address), None)); - } - - if let Some(ranges) = memory_ranges { - self.ensure_rtt_ranges_allowed(target_chip, &ranges)?; - return Ok((None, Some(ranges))); - } - - if self.config.security.restrict_memory_access { - return Ok((None, Some(self.configured_rtt_scan_ranges(target_chip)?))); - } - - Ok((None, None)) - } - - fn ensure_rtt_ranges_allowed( - &self, - target_chip: &str, - ranges: &[RttScanRange], - ) -> Result<(), McpError> { - if ranges.is_empty() { - return Err(McpError::internal_error( - "RTT memory ranges must not be empty.".to_string(), - None, - )); - } - - for (start, end) in ranges { - let size = end.checked_sub(*start).ok_or_else(|| { - McpError::internal_error( - format!( - "RTT memory range 0x{start:08X}..0x{end:08X} has an invalid end address." - ), - None, - ) - })?; - if size == 0 { - return Err(McpError::internal_error( - format!("RTT memory range 0x{start:08X}..0x{end:08X} is empty."), - None, - )); - } - let size = usize::try_from(size).map_err(|_| { - McpError::internal_error( - format!("RTT memory range 0x{start:08X}..0x{end:08X} is too large."), - None, - ) - })?; - self.ensure_memory_region_allowed_for_target(target_chip, *start, size, 'r')?; - } - - Ok(()) - } - - fn configured_rtt_scan_ranges(&self, target_chip: &str) -> Result, McpError> { - let target = self.target_config_for(target_chip).ok_or_else(|| { - McpError::internal_error( - format!( - "RTT scan is restricted, but target '{}' has no configured memory map.", - target_chip - ), - None, - ) - })?; - - let ranges: Result, _> = target - .memory_regions - .iter() - .filter(|region| region.access.contains('r')) - .filter(|region| { - !self.config.rtt.scan_ram_only || region.name.to_ascii_lowercase().contains("ram") - }) - .map(|region| { - let end_exclusive = region.end.checked_add(1).ok_or_else(|| { - McpError::internal_error( - format!("Memory region '{}' end address overflows.", region.name), - None, - ) - })?; - Ok((region.start, end_exclusive)) - }) - .collect(); - - let ranges = ranges?; - if ranges.is_empty() { - return Err(McpError::internal_error( - format!( - "RTT scan is restricted, but target '{}' has no readable configured ranges.", - target_chip - ), - None, - )); - } - - Ok(ranges) - } - - fn target_config_for(&self, target_chip: &str) -> Option<&TargetConfig> { - let target_chip_lower = target_chip.to_lowercase(); - self.config - .targets - .get(&target_chip_lower) - .or_else(|| self.config.targets.get(target_chip)) - .or_else(|| { - self.config.targets.values().find(|target| { - target.chip.eq_ignore_ascii_case(target_chip) - || target.name.eq_ignore_ascii_case(target_chip) - }) - }) - } - - fn resolve_allowed_file_path(&self, path: &str, max_size: usize) -> Result { - let path = Path::new(path); - let canonical = path.canonicalize().map_err(|e| { - McpError::internal_error( - format!("Failed to resolve file path '{}': {}", path.display(), e), - None, - ) - })?; - - let metadata = canonical.metadata().map_err(|e| { - McpError::internal_error( - format!( - "Failed to read metadata for '{}': {}", - canonical.display(), - e - ), - None, - ) - })?; - if !metadata.is_file() { - return Err(McpError::internal_error( - format!("Path '{}' is not a regular file.", canonical.display()), - None, - )); - } - - let file_size = metadata.len() as usize; - let max_size = max_size.min(self.config.security.max_file_size); - if file_size > max_size { - return Err(McpError::internal_error( - format!( - "File '{}' is {} bytes, exceeding configured limit {}.", - canonical.display(), - file_size, - max_size - ), - None, - )); - } - - if self.config.security.allowed_file_paths.is_empty() { - return Ok(canonical); - } - - let allowed = self - .config - .security - .allowed_file_paths - .iter() - .filter_map(|root| Path::new(root).canonicalize().ok()) - .any(|root| canonical.starts_with(root)); - - if allowed { - Ok(canonical) - } else { - Err(McpError::internal_error( - format!( - "File '{}' is outside configured allowed_file_paths.", - canonical.display() - ), - None, - )) - } - } -} - -impl Default for EmbeddedDebuggerToolHandler { - fn default() -> Self { - Self::new(5) - } -} - -#[tool_router] -impl EmbeddedDebuggerToolHandler { - // ============================================================================= - // Debugger Management Tools (4 tools) - // ============================================================================= - - #[tool(description = "List all available debug probes (J-Link, ST-Link, DAPLink, etc.)")] - async fn list_probes( - &self, - Parameters(_args): Parameters, - ) -> Result { - debug!("Listing available debug probes"); - - // Real probe-rs integration - let probes = Lister::new().list_all(); - let message = if probes.is_empty() { - "No debug probes found.\n\nPlease ensure your probe is connected and drivers are installed.\nSupported probes: J-Link, ST-Link, DAPLink, Black Magic Probe".to_string() - } else { - let mut result = format!("Found {} debug probe(s):\n\n", probes.len()); - - for (i, probe) in probes.iter().enumerate() { - result.push_str(&format!("{}. {}\n", i + 1, probe.identifier)); - result.push_str(&format!( - " VID:PID = {:04X}:{:04X}\n", - probe.vendor_id, probe.product_id - )); - - if let Some(serial) = &probe.serial_number { - result.push_str(&format!(" Serial: {}\n", serial)); - } - - result.push_str(&format!(" Probe Type: {:?}\n", probe.probe_type())); - result.push('\n'); - } - - result - }; - - info!("Listed {} debug probes", probes.len()); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - - #[tool(description = "Connect to a debug probe and target chip")] - async fn connect( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!( - "Connecting to probe '{}' and target '{}'", - args.probe_selector, args.target_chip - ); - - let session_slot = self - .session_slots - .clone() - .try_acquire_owned() - .map_err(|_| { - McpError::internal_error( - format!( - "Session limit exceeded. Maximum {} sessions allowed.", - self.max_sessions - ), - None, - ) - })?; - - // Real probe-rs implementation - let probes = Lister::new().list_all(); - - if probes.is_empty() { - return Err(McpError::internal_error( - "No debug probes found. Please connect a supported probe (J-Link, ST-Link, DAPLink, etc.)".to_string(), - None - )); - } - - let selected_probe = if args.probe_selector.to_lowercase() == "auto" { - probes.first() - } else { - probes - .iter() - .find(|p| p.identifier.contains(&args.probe_selector)) - }; - - match selected_probe { - Some(probe_info) => { - info!("Opening probe: {}", probe_info.identifier); - match probe_info.open() { - Ok(mut probe) => { - let actual_speed = probe.set_speed(args.speed_khz).map_err(|e| { - McpError::internal_error( - format!( - "Failed to set probe speed to {} kHz: {}", - args.speed_khz, e - ), - None, - ) - })?; - - let permissions = if self.flash_erase_allowed() { - Permissions::new().allow_erase_all() - } else { - Permissions::new() - }; - - let connect_under_reset = - args.connect_under_reset || self.config.debugger.connect_under_reset; - let halt_after_connect = - args.halt_after_connect || self.config.debugger.halt_on_connect; - - info!("Attaching to target: {}", args.target_chip); - let attach_result = if connect_under_reset { - probe.attach_under_reset(&args.target_chip, permissions) - } else { - probe.attach(&args.target_chip, permissions) - }; - - match attach_result { - Ok(mut session) => { - if halt_after_connect { - let mut core = session.core(0).map_err(|e| { - McpError::internal_error( - format!( - "Connected but failed to get core for halt: {}", - e - ), - None, - ) - })?; - core.halt(std::time::Duration::from_millis( - self.config.debugger.connection_timeout_ms, - )) - .map_err(|e| { - McpError::internal_error( - format!("Connected but failed to halt target: {}", e), - None, - ) - })?; - } - - let session_id = format!("session_{}", uuid::Uuid::new_v4()); - - let debug_session = DebugSession { - session_id: session_id.clone(), - probe_identifier: probe_info.identifier.clone(), - target_chip: args.target_chip.clone(), - created_at: chrono::Utc::now(), - session: Arc::new(tokio::sync::Mutex::new(session)), - rtt_manager: Arc::new(tokio::sync::Mutex::new( - RttManager::new(), - )), - _session_slot: session_slot, - }; - - // Store session - { - let mut sessions = self.sessions.write().await; - sessions.insert(session_id.clone(), Arc::new(debug_session)); - } - - let message = format!( - "Debug session established.\n\n\ - Session ID: {}\n\ - Probe: {} (VID:PID = {:04X}:{:04X})\n\ - Target: {}\n\ - Speed: {} kHz\n\ - Connect under reset: {}\n\ - Halted after connect: {}\n\ - Connected at: {}\n\n\ - Target connection established and ready for debugging.\n\ - Use this session ID for all debug operations.", - session_id, - probe_info.identifier, - probe_info.vendor_id, - probe_info.product_id, - args.target_chip, - actual_speed, - connect_under_reset, - halt_after_connect, - chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC") - ); - - info!("Created debug session: {}", session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!("Failed to attach to target '{}': {}", args.target_chip, e); - let error_msg = format!( - "Failed to attach to target '{}'\n\n\ - Error: {}\n\n\ - Suggestions:\n\ - - Check target chip name (try: STM32F407VGTx, nRF52840_xxAA)\n\ - - Ensure target is powered and connected\n\ - - Verify SWD/JTAG connections", - args.target_chip, e - ); - Err(McpError::internal_error(error_msg, None)) - } - } - } - Err(e) => { - error!("Failed to open probe '{}': {}", probe_info.identifier, e); - let error_msg = format!( - "Failed to open probe '{}'\n\nError: {}\n\n\ - Suggestions:\n\ - - Check probe drivers installation\n\ - - Verify USB connection\n\ - - Try disconnecting and reconnecting probe", - probe_info.identifier, e - ); - Err(McpError::internal_error(error_msg, None)) - } - } - } - None => { - let available_probes: Vec = probes - .iter() - .map(|p| format!("- {}", p.identifier)) - .collect(); - - let error_msg = format!( - "Probe '{}' not found\n\n\ - Available probes:\n{}\n\n\ - Use 'auto' to connect to first available probe.", - args.probe_selector, - available_probes.join("\n") - ); - Err(McpError::internal_error(error_msg, None)) - } - } - } - - #[tool(description = "Disconnect from a debug session")] - async fn disconnect( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!("Disconnecting session: {}", args.session_id); - - // Remove session from storage - let removed_session = { - let mut sessions = self.sessions.write().await; - sessions.remove(&args.session_id) - }; - - match removed_session { - Some(session) => { - let message = format!( - "Debug session disconnected successfully\n\n\ - Session ID: {}\n\ - Probe: {}\n\ - Target: {}\n\ - Duration: {:.1} minutes\n\n\ - probe-rs Session resources have been cleaned up.", - args.session_id, - session.probe_identifier, - session.target_chip, - (chrono::Utc::now() - session.created_at).num_seconds() as f64 / 60.0 - ); - - info!("Disconnected debug session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - None => { - let error_msg = format!( - "Session '{}' not found\n\nUse 'connect' to establish a debug session first", - args.session_id - ); - Err(McpError::internal_error(error_msg, None)) - } - } - } - - #[tool(description = "Get basic information about a debug session")] - async fn probe_info( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!("Getting probe info for session: {}", args.session_id); - - // Get session from storage - let session_arc = self.get_session(&args.session_id).await?; - - // Calculate session duration - let duration_minutes = - (chrono::Utc::now() - session_arc.created_at).num_seconds() as f64 / 60.0; - - let message = format!( - "Debug Session Information\n\n\ - Probe Information:\n\ - - Identifier: {}\n\ - - Connected: true\n\n\ - Target Information:\n\ - - Chip: {}\n\n\ - Session Status:\n\ - - Session ID: {}\n\ - - Created: {}\n\ - - Duration: {:.1} minutes\n\n\ - Session is active and ready for operations.", - session_arc.probe_identifier, - session_arc.target_chip, - args.session_id, - session_arc.created_at.format("%Y-%m-%d %H:%M:%S UTC"), - duration_minutes - ); - - info!("Retrieved probe info for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - - // ============================================================================= - // Target Control Tools (5 tools) - // ============================================================================= - - #[tool(description = "Halt the target CPU execution")] - async fn halt( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!("Halting target for session: {}", args.session_id); - - let session_arc = self.get_session(&args.session_id).await?; - - // Halt the target - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - match core.halt(std::time::Duration::from_millis(1000)) { - Ok(_) => { - // Get status after halt - match core.status() { - Ok(_status) => { - let pc = core - .read_core_reg(core.program_counter()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - let sp = core - .read_core_reg(core.stack_pointer()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - - let message = format!( - "Target halted successfully!\n\n\ - Session ID: {}\n\ - PC: 0x{:08X}\n\ - SP: 0x{:08X}\n\ - State: Halted\n", - args.session_id, pc, sp - ); - - info!("Halt completed for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - warn!("Failed to get status after halt: {}", e); - let message = format!( - "Target halted successfully!\n\n\ - Session ID: {}\n\ - State: Halted\n", - args.session_id - ); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - } - } - Err(e) => { - error!( - "Failed to halt target for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to halt target: {}", e), - None, - )) - } - } - } - } - - #[tool(description = "Resume target CPU execution")] - async fn run(&self, Parameters(args): Parameters) -> Result { - debug!("Running target for session: {}", args.session_id); - - let session_arc = self.get_session(&args.session_id).await?; - - // Resume the target - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - match core.run() { - Ok(_) => { - let message = format!( - "Target resumed execution successfully!\n\n\ - Session ID: {}\n\ - Status: Running\n\n\ - The target is now executing code. Use 'halt' to stop execution.", - args.session_id - ); - - info!("Run completed for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to run target for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to run target: {}", e), - None, - )) - } - } - } - } - - #[tool(description = "Reset the target CPU")] - async fn reset( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!("Resetting target for session: {}", args.session_id); - - if args.reset_type != "hardware" { - return Err(McpError::internal_error( - format!( - "Unsupported reset_type '{}'. probe-rs core reset is exposed as 'hardware' by this server.", - args.reset_type - ), - None, - )); - } - - let session_arc = self.get_session(&args.session_id).await?; - - // Reset the target - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - let reset_result = if args.halt_after_reset { - core.reset_and_halt(std::time::Duration::from_millis( - self.config.debugger.connection_timeout_ms, - )) - .map(|_| ()) - } else { - core.reset() - }; - - match reset_result { - Ok(_) => { - let pc = core - .read_core_reg(core.program_counter()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - let sp = core - .read_core_reg(core.stack_pointer()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - - let message = format!( - "Target reset completed successfully.\n\n\ - Session ID: {}\n\ - Reset type: {}\n\ - Halted after reset: {}\n\ - PC: 0x{:08X}\n\ - SP: 0x{:08X}\n\ - State: {}\n", - args.session_id, - args.reset_type, - args.halt_after_reset, - pc, - sp, - if args.halt_after_reset { - "Halted" - } else { - "Running" - } - ); - - info!("Reset completed for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to reset target for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to reset target: {}", e), - None, - )) - } - } - } - } - - #[tool(description = "Execute a single instruction step")] - async fn step( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!("Single stepping target for session: {}", args.session_id); - - let session_arc = self.get_session(&args.session_id).await?; - - // Single step the target - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - match core.step() { - Ok(_) => { - let pc = core - .read_core_reg(core.program_counter()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - let sp = core - .read_core_reg(core.stack_pointer()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - - let message = format!( - "Single step completed successfully!\n\n\ - Session ID: {}\n\ - PC: 0x{:08X}\n\ - SP: 0x{:08X}\n\ - State: Halted\n", - args.session_id, pc, sp - ); - - info!("Step completed for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to step target for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to step target: {}", e), - None, - )) - } - } - } - } - - #[tool(description = "Get current status of the target CPU and debug session")] - async fn get_status( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!("Getting status for session: {}", args.session_id); - - let session_arc = self.get_session(&args.session_id).await?; - - // Get target status - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - match core.status() { - Ok(status) => { - let pc = core - .read_core_reg(core.program_counter()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - let sp = core - .read_core_reg(core.stack_pointer()) - .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) - .unwrap_or(0); - - let is_halted = matches!(status, CoreStatus::Halted(_)); - let halt_reason = match status { - CoreStatus::Halted(reason) => format!("{:?}", reason), - CoreStatus::Running => "N/A".to_string(), - _ => "Unknown".to_string(), - }; - - let message = format!( - "Debug Session Status\n\n\ - Core Information:\n\ - - PC: 0x{:08X}\n\ - - SP: 0x{:08X}\n\ - - State: {}\n\ - - Halt reason: {}\n\n\ - Session Information:\n\ - - ID: {}\n\ - - Connected: true\n\ - - Target: {}\n\ - - Probe: {}\n\ - - Duration: {:.1} minutes\n", - pc, - sp, - if is_halted { "Halted" } else { "Running" }, - halt_reason, - args.session_id, - session_arc.target_chip, - session_arc.probe_identifier, - (chrono::Utc::now() - session_arc.created_at).num_seconds() as f64 / 60.0 - ); - - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to get core status for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to get core status: {}", e), - None, - )) - } - } - } - } - - // ============================================================================= - // Memory Operation Tools (2 tools) - // ============================================================================= - - #[tool(description = "Read memory from the target")] - async fn read_memory( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!( - "Reading memory for session: {} at address {}", - args.session_id, args.address - ); - - // Parse address - let address = match parse_address(&args.address) { - Ok(addr) => addr, - Err(e) => { - error!("Invalid address '{}': {}", args.address, e); - return Err(McpError::internal_error( - format!("Invalid address '{}': {}", args.address, e), - None, - )); - } - }; - - let session_arc = self.get_session(&args.session_id).await?; - self.ensure_memory_read_allowed(&session_arc, address, args.size)?; - - // Read memory - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - let mut data = vec![0u8; args.size]; - match core.read(address, &mut data) { - Ok(_) => { - debug!("Read {} bytes from address 0x{:08X}", data.len(), address); - - let formatted_data = format_memory_data(&data, &args.format, address); - let message = format!( - "Memory read completed successfully.\n\n\ - Session ID: {}\n\ - Address: 0x{:08X}\n\ - Size: {} bytes\n\ - Format: {}\n\n\ - Data:\n{}", - args.session_id, address, args.size, args.format, formatted_data - ); - - info!("Memory read completed for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to read memory for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to read memory: {}", e), - None, - )) - } - } - } - } - - #[tool(description = "Write memory to the target")] - async fn write_memory( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!( - "Writing memory for session: {} at address {}", - args.session_id, args.address - ); - - // Parse address - let address = match parse_address(&args.address) { - Ok(addr) => addr, - Err(e) => { - error!("Invalid address '{}': {}", args.address, e); - return Err(McpError::internal_error( - format!("Invalid address '{}': {}", args.address, e), - None, - )); - } - }; - - // Parse data based on format - let data = match parse_data(&args.data, &args.format) { - Ok(data) => data, - Err(e) => { - error!("Invalid data '{}': {}", args.data, e); - return Err(McpError::internal_error( - format!("Invalid data '{}': {}", args.data, e), - None, - )); - } - }; - - let session_arc = self.get_session(&args.session_id).await?; - self.ensure_memory_write_allowed(&session_arc, address, data.len())?; - - // Write memory - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - match core.write(address, &data) { - Ok(_) => { - let message = format!( - "Memory write completed successfully.\n\n\ - Session ID: {}\n\ - Address: 0x{:08X}\n\ - Data: {}\n\ - Format: {}\n\ - Bytes written: {}", - args.session_id, - address, - args.data, - args.format, - data.len() - ); - - info!("Memory write completed for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to write memory for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to write memory: {}", e), - None, - )) - } - } - } - } - - // ============================================================================= - // Breakpoint Tools (2 tools) - // ============================================================================= - - #[tool(description = "Set a breakpoint at the specified address")] - async fn set_breakpoint( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!( - "Setting breakpoint for session: {} at address {}", - args.session_id, args.address - ); - - if args.breakpoint_type != "hardware" { - return Err(McpError::internal_error( - format!( - "Unsupported breakpoint_type '{}'. Only hardware breakpoints are implemented.", - args.breakpoint_type - ), - None, - )); - } - - // Parse address - let address = match parse_address(&args.address) { - Ok(addr) => addr, - Err(e) => { - error!("Invalid address '{}': {}", args.address, e); - return Err(McpError::internal_error( - format!("Invalid address '{}': {}", args.address, e), - None, - )); - } - }; - - let session_arc = self.get_session(&args.session_id).await?; - - // Set breakpoint - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - match core.set_hw_breakpoint(address) { - Ok(_) => { - let message = format!( - "Breakpoint set successfully.\n\n\ - Session ID: {}\n\ - Address: 0x{:08X}\n\ - Type: Hardware breakpoint\n\n\ - The target will halt when execution reaches this address.", - args.session_id, address - ); - - info!( - "Breakpoint set for session: {} at 0x{:08X}", - args.session_id, address - ); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to set breakpoint for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to set breakpoint: {}", e), - None, - )) - } - } - } - } - - #[tool(description = "Clear a breakpoint at the specified address")] - async fn clear_breakpoint( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!( - "Clearing breakpoint for session: {} at address {}", - args.session_id, args.address - ); - - // Parse address - let address = match parse_address(&args.address) { - Ok(addr) => addr, - Err(e) => { - error!("Invalid address '{}': {}", args.address, e); - return Err(McpError::internal_error( - format!("Invalid address '{}': {}", args.address, e), - None, - )); - } - }; - - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; - - // Clear breakpoint - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - error!("Failed to get core for session {}: {}", args.session_id, e); - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )); - } - }; - - match core.clear_hw_breakpoint(address) { - Ok(_) => { - let message = format!( - "Breakpoint cleared successfully.\n\n\ - Session ID: {}\n\ - Address: 0x{:08X}\n\n\ - The breakpoint has been removed.", - args.session_id, address - ); - - info!( - "Breakpoint cleared for session: {} at 0x{:08X}", - args.session_id, address - ); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to clear breakpoint for session {}: {}", - args.session_id, e - ); - Err(McpError::internal_error( - format!("Failed to clear breakpoint: {}", e), - None, - )) - } - } - } - } - - // ============================================================================= - // RTT Communication Tools (5 tools) - // ============================================================================= - - #[tool(description = "Attach to RTT (Real-Time Transfer) for communication with target")] - async fn rtt_attach( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!("Attaching RTT for session: {}", args.session_id); - - let session_arc = self.get_session(&args.session_id).await?; - - // Parse control block address if provided - let control_block_address = if let Some(addr_str) = args.control_block_address { - match parse_address(&addr_str) { - Ok(addr) => Some(addr), - Err(e) => { - let error_msg = format!("Invalid control block address '{}': {}", addr_str, e); - return Err(McpError::internal_error(error_msg, None)); - } - } - } else { - None - }; - - // Parse memory ranges if provided - let memory_ranges = if let Some(ranges) = args.memory_ranges { - let mut parsed_ranges = Vec::new(); - for range in ranges { - let start = parse_address(&range.start).map_err(|e| { - McpError::internal_error( - format!("Invalid start address '{}': {}", range.start, e), - None, - ) - })?; - let end = parse_address(&range.end).map_err(|e| { - McpError::internal_error( - format!("Invalid end address '{}': {}", range.end, e), - None, - ) - })?; - parsed_ranges.push((start, end)); - } - Some(parsed_ranges) - } else { - None - }; - - let (control_block_address, memory_ranges) = self.prepare_rtt_scan_region( - &session_arc.target_chip, - control_block_address, - memory_ranges, - )?; - - // Attach RTT - { - let mut rtt_manager = session_arc.rtt_manager.lock().await; - match rtt_manager - .attach( - session_arc.session.clone(), - control_block_address, - memory_ranges, - ) - .await - { - Ok(_) => { - let up_channels = rtt_manager.up_channel_count(); - let down_channels = rtt_manager.down_channel_count(); - - let message = format!( - "RTT attached successfully!\n\n\ - Session ID: {}\n\ - Up Channels (Target to Host): {}\n\ - Down Channels (Host to Target): {}\n\n\ - RTT is now ready for real-time communication with the target.\n\ - Use 'rtt_read' to read from target and 'rtt_write' to send data to target.", - args.session_id, up_channels, down_channels - ); - - info!("RTT attached successfully for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to attach RTT for session {}: {}", - args.session_id, e - ); - let error_msg = format!( - "Failed to attach RTT\n\n\ - Session ID: {}\n\ - Error: {}\n\n\ - Suggestions:\n\ - - Ensure the target firmware has RTT enabled and initialized\n\ - - Check that the target is halted\n\ - - Verify memory ranges if specified\n\ - - Try different control block address if known", - args.session_id, e - ); - Err(McpError::internal_error(error_msg, None)) - } - } - } - } - - #[tool(description = "Detach from RTT communication")] - async fn rtt_detach( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!("Detaching RTT for session: {}", args.session_id); - - let session_arc = self.get_session(&args.session_id).await?; - - // Detach RTT - { - let mut rtt_manager = session_arc.rtt_manager.lock().await; - match rtt_manager.detach().await { - Ok(_) => { - let message = format!( - "RTT detached successfully\n\n\ - Session ID: {}\n\n\ - RTT communication has been closed.", - args.session_id - ); - - info!("RTT detached successfully for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to detach RTT for session {}: {}", - args.session_id, e - ); - let error_msg = format!("Failed to detach RTT: {}", e); - Err(McpError::internal_error(error_msg, None)) - } - } - } - } - - #[tool(description = "Read data from RTT up channel (target to host)")] - async fn rtt_read( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!( - "Reading from RTT channel {} for session: {}", - args.channel, args.session_id - ); - - let session_arc = self.get_session(&args.session_id).await?; - if args.max_bytes == 0 { - return Err(McpError::internal_error( - "max_bytes must be greater than zero.".to_string(), - None, - )); - } - if args.max_bytes > self.config.rtt.buffer_size { - return Err(McpError::internal_error( - format!( - "max_bytes {} exceeds configured RTT buffer size {}.", - args.max_bytes, self.config.rtt.buffer_size - ), - None, - )); - } - - // Read from RTT - { - let mut rtt_manager = session_arc.rtt_manager.lock().await; - if !rtt_manager.is_attached() { - let error_msg = format!( - "RTT not attached for session '{}'\n\nUse 'rtt_attach' first", - args.session_id - ); - return Err(McpError::internal_error(error_msg, None)); - } - - match rtt_manager - .read_channel(args.channel, args.max_bytes, args.timeout_ms) - .await - { - Ok(data) => { - let data_len = data.len(); - let data_str = if data.is_empty() { - "No data available".to_string() - } else { - // Try to decode as UTF-8, fall back to hex if not valid - match String::from_utf8(data.clone()) { - Ok(text) => { - if text - .chars() - .all(|c| c.is_ascii_graphic() || c.is_ascii_whitespace()) - { - format!("Text: {}", text) - } else { - format!("Mixed: {} (hex: {})", text, hex::encode(&data)) - } - } - Err(_) => format!("Binary data (hex): {}", hex::encode(&data)), - } - }; - - let message = format!( - "RTT Read from Channel {}\n\n\ - Session ID: {}\n\ - Bytes Read: {}\n\n\ - Data:\n{}", - args.channel, args.session_id, data_len, data_str - ); - - debug!( - "Read {} bytes from RTT channel {} for session: {}", - data_len, args.channel, args.session_id - ); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to read from RTT channel {} for session {}: {}", - args.channel, args.session_id, e - ); - let error_msg = format!( - "Failed to read from RTT channel {}\n\n\ - Session ID: {}\n\ - Error: {}", - args.channel, args.session_id, e - ); - Err(McpError::internal_error(error_msg, None)) - } - } - } - } - - #[tool(description = "Write data to RTT down channel (host to target)")] - async fn rtt_write( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!( - "Writing to RTT channel {} for session: {}", - args.channel, args.session_id - ); - - // Get session from storage - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; - - // Parse data based on encoding - let data_bytes = match args.encoding.as_str() { - "utf8" => args.data.as_bytes().to_vec(), - "hex" => match hex::decode(&args.data) { - Ok(bytes) => bytes, - Err(e) => { - let error_msg = format!("Invalid hex data '{}': {}", args.data, e); - return Err(McpError::internal_error(error_msg, None)); - } - }, - "binary" => { - // Parse binary string like "10110011 11001100" - let binary_str = args.data.replace(' ', ""); - if binary_str.len() % 8 != 0 { - let error_msg = - format!("Binary data must be multiple of 8 bits: '{}'", args.data); - return Err(McpError::internal_error(error_msg, None)); - } - - let mut bytes = Vec::new(); - for chunk in binary_str.chars().collect::>().chunks(8) { - let byte_str: String = chunk.iter().collect(); - match u8::from_str_radix(&byte_str, 2) { - Ok(byte) => bytes.push(byte), - Err(e) => { - let error_msg = format!("Invalid binary byte '{}': {}", byte_str, e); - return Err(McpError::internal_error(error_msg, None)); - } - } - } - bytes - } - _ => { - let error_msg = format!( - "Unsupported encoding '{}'. Use 'utf8', 'hex', or 'binary'", - args.encoding - ); - return Err(McpError::internal_error(error_msg, None)); - } - }; - - // Write to RTT - { - let mut rtt_manager = session_arc.rtt_manager.lock().await; - if !rtt_manager.is_attached() { - let error_msg = format!( - "RTT not attached for session '{}'\n\nUse 'rtt_attach' first", - args.session_id - ); - return Err(McpError::internal_error(error_msg, None)); - } - - match rtt_manager.write_channel(args.channel, &data_bytes).await { - Ok(bytes_written) => { - let message = format!( - "RTT Write to Channel {}\n\n\ - Session ID: {}\n\ - Data: {}\n\ - Encoding: {}\n\ - Bytes Written: {}\n\n\ - Data sent successfully to target.", - args.channel, args.session_id, args.data, args.encoding, bytes_written - ); - - info!( - "Wrote {} bytes to RTT channel {} for session: {}", - bytes_written, args.channel, args.session_id - ); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Failed to write to RTT channel {} for session {}: {}", - args.channel, args.session_id, e - ); - let error_msg = format!( - "Failed to write to RTT channel {}\n\n\ - Session ID: {}\n\ - Error: {}", - args.channel, args.session_id, e - ); - Err(McpError::internal_error(error_msg, None)) - } - } - } - } - - #[tool(description = "List available RTT channels")] - async fn rtt_channels( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!("Listing RTT channels for session: {}", args.session_id); - - // Get session from storage - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; - - // List RTT channels - { - let rtt_manager = session_arc.rtt_manager.lock().await; - if !rtt_manager.is_attached() { - let error_msg = format!( - "RTT not attached for session '{}'\n\nUse 'rtt_attach' first", - args.session_id - ); - return Err(McpError::internal_error(error_msg, None)); - } - - let channels = rtt_manager.get_channels(); - let channel_count = channels.len(); - - if channels.is_empty() { - let message = format!( - "📋 RTT Channels\n\n\ - Session ID: {}\n\n\ - No RTT channels available.", - args.session_id - ); - return Ok(CallToolResult::success(vec![Content::text(message)])); - } - - let mut message = format!("📋 RTT Channels\n\nSession ID: {}\n\n", args.session_id); - - // Group channels by direction - let mut up_channels = Vec::new(); - let mut down_channels = Vec::new(); - - for channel in &channels { - match channel.direction { - crate::rtt::ChannelDirection::Up => up_channels.push(channel), - crate::rtt::ChannelDirection::Down => down_channels.push(channel), - } - } - - if !up_channels.is_empty() { - message.push_str("Up Channels (Target to Host):\n"); - for channel in up_channels { - message.push_str(&format!( - " {}. {} (Size: {} bytes, Mode: {})\n", - channel.id, channel.name, channel.buffer_size, channel.mode - )); - } - message.push('\n'); - } - - if !down_channels.is_empty() { - message.push_str("Down Channels (Host to Target):\n"); - for channel in down_channels { - message.push_str(&format!( - " {}. {} (Size: {} bytes, Mode: {})\n", - channel.id, channel.name, channel.buffer_size, channel.mode - )); - } - } - - info!( - "Listed {} RTT channels for session: {}", - channel_count, args.session_id - ); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - } - - // ============================================================================= - // Flash Programming Tools (4 tools) - // ============================================================================= - - #[tool(description = "Erase flash memory sectors or entire chip")] - async fn flash_erase( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!( - "Flash erase for session: {}, type: {}", - args.session_id, args.erase_type - ); - - self.ensure_flash_erase_allowed()?; - let session_arc = self.get_session(&args.session_id).await?; - - // Parse erase type and parameters - let erase_type = match args.erase_type.as_str() { - "all" => crate::flash::EraseType::All, - "sectors" => { - let address = match args.address { - Some(addr_str) => { - parse_address(&addr_str).map_err(|e| McpError::internal_error(e, None))? - } - None => { - return Err(McpError::internal_error( - "Address required for sector erase".to_string(), - None, - )) - } - }; - let size = match args.size { - Some(sz) => sz as usize, - None => { - return Err(McpError::internal_error( - "Size required for sector erase".to_string(), - None, - )) - } - }; - crate::flash::EraseType::Sectors { address, size } - } - _ => { - return Err(McpError::internal_error( - format!("Invalid erase type: {}", args.erase_type), - None, - )) - } - }; - - // Perform erase operation - { - let mut session = session_arc.session.lock().await; - match crate::flash::FlashManager::erase_flash(&mut session, erase_type).await { - Ok(result) => { - let message = format!( - "Flash erase completed successfully.\n\n\ - Session ID: {}\n\ - Erase Type: {}\n\ - Duration: {}ms\n\ - {}\n\n\ - Flash memory has been erased and is ready for programming.", - args.session_id, - args.erase_type, - result.erase_time_ms, - match result.sectors_erased { - Some(count) => format!("Sectors Erased: {}", count), - None => "Full chip erased".to_string(), - } - ); - - info!("Flash erase completed for session: {}", args.session_id); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!("Flash erase failed for session {}: {}", args.session_id, e); - let error_msg = format!( - "Flash erase failed\n\n\ - Session ID: {}\n\ - Error: {}\n\n\ - Suggestions:\n\ - - Check if flash is write-protected\n\ - - Ensure target is halted\n\ - - Verify flash address range", - args.session_id, e - ); - Err(McpError::internal_error(error_msg, None)) - } - } - } - } - - #[tool(description = "Program file to flash memory (supports ELF, HEX, BIN)")] - async fn flash_program( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!( - "Flash program for session: {}, file: {}", - args.session_id, args.file_path - ); - - self.ensure_flash_erase_allowed()?; - - let session_arc = self.get_session(&args.session_id).await?; - - // Parse file path and format - let file_path = - self.resolve_allowed_file_path(&args.file_path, self.config.flash.max_binary_size)?; - let format = match args.format.as_str() { - "auto" => crate::flash::FileFormat::Auto, - "elf" => crate::flash::FileFormat::Elf, - "hex" => crate::flash::FileFormat::Hex, - "bin" => crate::flash::FileFormat::Bin, - _ => { - return Err(McpError::internal_error( - format!("Unsupported format: {}", args.format), - None, - )) - } - }; - - // Parse base address if provided - let base_address = if let Some(addr_str) = args.base_address { - Some(parse_address(&addr_str).map_err(|e| McpError::internal_error(e, None))?) - } else { - None - }; - - // Perform programming operation - { - let mut session = session_arc.session.lock().await; - let verify = args.verify || self.config.flash.verify_after_program; - match crate::flash::FlashManager::program_file( - &mut session, - &file_path, - format, - base_address, - verify, - ) - .await - { - Ok(result) => { - let message = format!( - "Flash programming completed successfully.\n\n\ - Session ID: {}\n\ - File: {}\n\ - Format: {}\n\ - Bytes Programmed: {}\n\ - Duration: {}ms\n\ - Verification: {}\n\n\ - Firmware has been programmed to flash memory.", - args.session_id, - file_path.display(), - args.format, - result.bytes_programmed, - result.programming_time_ms, - match result.verification_result { - Some(true) => "Passed", - Some(false) => "Failed", - None => "Not performed", - } - ); - - info!( - "Flash programming completed for session: {}", - args.session_id - ); - Ok(CallToolResult::success(vec![Content::text(message)])) - } - Err(e) => { - error!( - "Flash programming failed for session {}: {}", - args.session_id, e - ); - let error_msg = format!( - "Flash programming failed\n\n\ - Session ID: {}\n\ - File: {}\n\ - Error: {}\n\n\ - Suggestions:\n\ - - Check file exists and is readable\n\ - - Verify file format is correct\n\ - - Ensure flash is erased first\n\ - - Check target memory map", - args.session_id, args.file_path, e - ); - Err(McpError::internal_error(error_msg, None)) - } - } - } - } - - #[tool(description = "Verify flash memory contents")] - async fn flash_verify( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!("Flash verify for session: {}", args.session_id); - - let session_arc = { - let sessions = self.sessions.read().await; - match sessions.get(&args.session_id) { - Some(session) => session.clone(), - None => { - let error_msg = format!("Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); - return Err(McpError::internal_error(error_msg, None)); - } - } - }; - - // Parse address - let address = - parse_address(&args.address).map_err(|e| McpError::internal_error(e, None))?; - - // Get expected data - let expected_data = if let Some(file_path) = &args.file_path { - let file_path = - self.resolve_allowed_file_path(file_path, self.config.flash.max_binary_size)?; - let extension = file_path - .extension() - .and_then(|ext| ext.to_str()) - .map(|ext| ext.to_ascii_lowercase()); - if matches!(extension.as_deref(), Some("elf" | "hex")) { - return Err(McpError::internal_error( - format!( - "flash_verify compares raw bytes only; '{}' must be verified with a raw BIN file or hex data.", - file_path.display() - ), - None, - )); - } - std::fs::read(&file_path).map_err(|e| { - McpError::internal_error( - format!("Failed to read file {}: {}", file_path.display(), e), - None, - ) - })? - } else if let Some(hex_data) = &args.data { - // Parse hex data - match parse_data(hex_data, "hex") { - Ok(data) => data, - Err(e) => { - return Err(McpError::internal_error( - format!("Invalid hex data: {}", e), - None, - )) - } - } - } else { - return Err(McpError::internal_error( - "Either file_path or data must be provided".to_string(), - None, - )); - }; - - let verify_size = args.size as usize; - if expected_data.len() < verify_size { - return Err(McpError::internal_error( - format!( - "Expected data has {} bytes, fewer than requested verify size {}.", - expected_data.len(), - verify_size - ), - None, - )); - } - - let expected_data = if expected_data.len() > verify_size { - &expected_data[..verify_size] - } else { - &expected_data - }; - - // Perform verification - { - let mut session = session_arc.session.lock().await; - match crate::flash::FlashManager::verify_flash(&mut session, expected_data, address) - .await - { - Ok(result) => { - let message = if result.success { - format!( - "Flash verification successful.\n\n\ - Session ID: {}\n\ - Address: 0x{:08X}\n\ - Bytes Verified: {}\n\n\ - All flash contents match expected data.", - args.session_id, address, result.bytes_verified - ) - } else { - let mut message = format!( - "Flash verification failed.\n\n\ - Session ID: {}\n\ - Address: 0x{:08X}\n\ - Bytes Verified: {}\n\ - Mismatches: {}\n\n\ - First {} mismatches:\n", - args.session_id, - address, - result.bytes_verified, - result.mismatches.len(), - std::cmp::min(10, result.mismatches.len()) - ); - - for (i, mismatch) in result.mismatches.iter().take(10).enumerate() { - message.push_str(&format!( - " {}. 0x{:08X}: expected 0x{:02X}, got 0x{:02X}\n", - i + 1, - mismatch.address, - mismatch.expected, - mismatch.actual - )); - } - - if result.mismatches.len() > 10 { - message.push_str(&format!( - " ... and {} more mismatches\n", - result.mismatches.len() - 10 - )); - } - - message - }; - - info!( - "Flash verification completed for session: {}", - args.session_id - ); - if result.success { - Ok(CallToolResult::success(vec![Content::text(message)])) - } else { - Ok(CallToolResult::error(vec![Content::text(message)])) - } - } - Err(e) => { - error!( - "Flash verification failed for session {}: {}", - args.session_id, e - ); - let error_msg = format!( - "Flash verification error\n\n\ - Session ID: {}\n\ - Error: {}", - args.session_id, e - ); - Err(McpError::internal_error(error_msg, None)) - } - } - } - } - - #[tool(description = "Firmware deployment helper: erase, program, verify, run, and attach RTT")] - async fn run_firmware( - &self, - Parameters(args): Parameters, - ) -> Result { - debug!( - "Run firmware for session: {}, file: {}", - args.session_id, args.file_path - ); - - self.ensure_flash_erase_allowed()?; - let session_arc = self.get_session(&args.session_id).await?; - let file_path = - self.resolve_allowed_file_path(&args.file_path, self.config.flash.max_binary_size)?; - - let mut status_messages = Vec::new(); - let start_time = std::time::Instant::now(); - - // Step 1: Erase flash - status_messages.push("Step 1/5: Erasing flash memory...".to_string()); - { - let mut session = session_arc.session.lock().await; - match crate::flash::FlashManager::erase_flash( - &mut session, - crate::flash::EraseType::All, - ) - .await - { - Ok(_) => status_messages.push("Flash erased successfully".to_string()), - Err(e) => { - let error_msg = format!("Flash erase failed: {}", e); - status_messages.push(error_msg.clone()); - return Err(McpError::internal_error( - format!("{}\n\n{}", status_messages.join("\n"), error_msg), - None, - )); - } - } - } - - // Step 2: Program firmware - status_messages.push("Step 2/5: Programming firmware...".to_string()); - let format = match args.format.as_str() { - "auto" => crate::flash::FileFormat::Auto, - "elf" => crate::flash::FileFormat::Elf, - "hex" => crate::flash::FileFormat::Hex, - "bin" => crate::flash::FileFormat::Bin, - _ => { - return Err(McpError::internal_error( - format!("Unsupported format: {}", args.format), - None, - )) - } - }; - - { - let mut session = session_arc.session.lock().await; - match crate::flash::FlashManager::program_file( - &mut session, - &file_path, - format, - None, - self.config.flash.verify_after_program, - ) - .await - { - Ok(result) => { - status_messages.push(format!("Programmed {} bytes", result.bytes_programmed)) - } - Err(e) => { - let error_msg = format!("Programming failed: {}", e); - status_messages.push(error_msg.clone()); - return Err(McpError::internal_error( - format!("{}\n\n{}", status_messages.join("\n"), error_msg), - None, - )); - } - } - } - - // Step 3: Reset and run - if args.reset_after_flash { - status_messages.push("Step 3/5: Resetting target...".to_string()); - { - let mut session = session_arc.session.lock().await; - let mut core = match session.core(0) { - Ok(core) => core, - Err(e) => { - return Err(McpError::internal_error( - format!("Failed to get core: {}", e), - None, - )) - } - }; - - match core.reset() { - Ok(_) => { - status_messages.push("Target reset successfully".to_string()); - // Run the target - match core.run() { - Ok(_) => status_messages.push("Target running".to_string()), - Err(e) => { - let error_msg = format!("Run after reset failed: {}", e); - status_messages.push(error_msg.clone()); - return Err(McpError::internal_error( - format!("{}\n\n{}", status_messages.join("\n"), error_msg), - None, - )); - } - } - } - Err(e) => { - let error_msg = format!("Reset failed: {}", e); - status_messages.push(error_msg.clone()); - return Err(McpError::internal_error( - format!("{}\n\n{}", status_messages.join("\n"), error_msg), - None, - )); - } - } - } - } - - // Step 4: Attach RTT (if requested) - Mimic probe-rs run behavior - if args.attach_rtt { - status_messages.push("Step 4/5: Attaching RTT...".to_string()); - - let timeout = tokio::time::Duration::from_millis(args.rtt_timeout_ms as u64); - let started = tokio::time::Instant::now(); - let mut rtt_attached = false; - let mut last_rtt_error = None; - let mut attempt = 1; - - loop { - if attempt > 1 { - let remaining = timeout.saturating_sub(started.elapsed()); - if remaining.is_zero() { - break; - } - tokio::time::sleep(remaining.min(tokio::time::Duration::from_millis(500))) - .await; - } - - // Try RTT attachment with different strategies (probe-rs style optimization) - let mut rtt_manager = session_arc.rtt_manager.lock().await; - let rtt_result = match attempt { - 1..=2 => { - // First 2 attempts: ELF symbol detection (probe-rs priority method) - debug!( - "RTT attempt {}: Using ELF symbol detection (probe-rs style)", - attempt - ); - rtt_manager - .attach_with_elf(session_arc.session.clone(), &file_path) - .await - } - 3..=5 => { - // Attempts 3-5: standard attach, let probe-rs auto-scan memory - debug!("RTT attempt {}: Using standard memory map scan", attempt); - rtt_manager - .attach(session_arc.session.clone(), None, None) - .await - } - 6..=7 => { - // Attempts 6-7: try STM32G4 specific memory ranges - debug!( - "RTT attempt {}: Using STM32G4 specific memory ranges", - attempt - ); - let stm32g4_ranges = vec![ - (0x20000000, 0x20004000), // SRAM1 first half: 16KB - most likely RTT location - (0x20004000, 0x20008000), // SRAM1 second half: 16KB - (0x20008000, 0x2000A000), // SRAM2: 8KB - ]; - rtt_manager - .attach(session_arc.session.clone(), None, Some(stm32g4_ranges)) - .await - } - _ => { - // Last attempt: try common RTT control block addresses - let cb_addr = 0x20000000; - debug!( - "RTT attempt {}: Using specific control block address 0x{:08X}", - attempt, cb_addr - ); - rtt_manager - .attach(session_arc.session.clone(), Some(cb_addr), None) - .await - } - }; - - match rtt_result { - Ok(_) => { - let up_channels = rtt_manager.up_channel_count(); - let down_channels = rtt_manager.down_channel_count(); - status_messages.push(format!( - "RTT attached on attempt {} ({} up, {} down channels)", - attempt, up_channels, down_channels - )); - info!("RTT successfully attached after {} attempts!", attempt); - rtt_attached = true; - break; - } - Err(e) => { - debug!("RTT attach attempt {} failed: {}", attempt, e); - last_rtt_error = Some(e.to_string()); - } - } - - if args.rtt_timeout_ms == 0 || started.elapsed() >= timeout { - break; - } - attempt += 1; - } - - if !rtt_attached { - let error_msg = format!( - "RTT attach failed within {}ms after {} attempt(s): {}", - args.rtt_timeout_ms, - attempt, - last_rtt_error.unwrap_or_else(|| "timeout expired".to_string()) - ); - status_messages.push(error_msg.clone()); - warn!("{}", error_msg); - return Err(McpError::internal_error( - format!("{}\n\n{}", status_messages.join("\n"), error_msg), - None, - )); - } - - info!("RTT connected successfully, allowing channel stabilization..."); - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - } - - status_messages.push("Step 5/5: Finalizing...".to_string()); - let elapsed = start_time.elapsed(); - - let message = format!( - "Firmware deployment completed.\n\n\ - Session ID: {}\n\ - File: {}\n\ - Format: {}\n\ - Total Time: {:.1}s\n\n\ - Status:\n{}\n\n\ - Firmware is now running on target.\n\ - {}", - args.session_id, - file_path.display(), - args.format, - elapsed.as_secs_f64(), - status_messages.join("\n"), - if args.attach_rtt { - "Use 'rtt_read' to monitor target output." - } else { - "Use 'rtt_attach' to enable real-time communication." - } - ); - - info!( - "Firmware deployment completed for session: {} in {:.1}s", - args.session_id, - elapsed.as_secs_f64() - ); - Ok(CallToolResult::success(vec![Content::text(message)])) - } -} - -// ============================================================================= -// Utility Functions -// ============================================================================= - -/// Parse address string (hex or decimal) to u64 -fn parse_address(addr_str: &str) -> Result { - let addr_str = addr_str.trim(); - - if addr_str.starts_with("0x") || addr_str.starts_with("0X") { - u64::from_str_radix(&addr_str[2..], 16).map_err(|e| format!("Invalid hex address: {}", e)) - } else { - addr_str - .parse::() - .map_err(|e| format!("Invalid decimal address: {}", e)) - } -} - -/// Parse data string based on format -fn parse_data(data_str: &str, format: &str) -> Result, String> { - match format { - "hex" => { - // Remove spaces and 0x prefixes - let clean_str = data_str - .replace(" ", "") - .replace("0x", "") - .replace("0X", ""); - if (clean_str.len() & 1) != 0 { - return Err("Hex data must have even number of characters".to_string()); - } - - (0..clean_str.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&clean_str[i..i + 2], 16)) - .collect::, _>>() - .map_err(|e| format!("Invalid hex data: {}", e)) - } - "ascii" => Ok(data_str.as_bytes().to_vec()), - "words32" => { - let words: Result, _> = data_str - .split_whitespace() - .map(|s| { - if s.starts_with("0x") || s.starts_with("0X") { - u32::from_str_radix(&s[2..], 16) - } else { - s.parse::() - } - }) - .collect(); - - match words { - Ok(words) => { - let mut data = Vec::new(); - for word in words { - data.extend_from_slice(&word.to_le_bytes()); - } - Ok(data) - } - Err(e) => Err(format!("Invalid word32 data: {}", e)), - } - } - "words16" => { - let words: Result, _> = data_str - .split_whitespace() - .map(|s| { - if s.starts_with("0x") || s.starts_with("0X") { - u16::from_str_radix(&s[2..], 16) - } else { - s.parse::() - } - }) - .collect(); - - match words { - Ok(words) => { - let mut data = Vec::new(); - for word in words { - data.extend_from_slice(&word.to_le_bytes()); - } - Ok(data) - } - Err(e) => Err(format!("Invalid word16 data: {}", e)), - } - } - _ => Err(format!("Unsupported data format: {}", format)), - } -} - -/// Format memory data for display -fn format_memory_data(data: &[u8], format: &str, base_address: u64) -> String { - match format { - "hex" => { - let mut result = String::new(); - for (i, chunk) in data.chunks(16).enumerate() { - let addr = base_address + (i * 16) as u64; - result.push_str(&format!("0x{:08X}: ", addr)); - - // Hex bytes - for (j, byte) in chunk.iter().enumerate() { - if j == 8 { - result.push(' '); - } - result.push_str(&format!("{:02X} ", byte)); - } - - // Pad if needed - if chunk.len() < 16 { - let padding = (16 - chunk.len()) * 3 + (if chunk.len() <= 8 { 1 } else { 0 }); - result.push_str(&" ".repeat(padding)); - } - - // ASCII representation - result.push_str("| "); - for byte in chunk { - if byte.is_ascii_graphic() || *byte == b' ' { - result.push(*byte as char); - } else { - result.push('.'); - } - } - result.push('\n'); - } - result - } - "binary" => data - .iter() - .map(|b| format!("{:08b}", b)) - .collect::>() - .join(" "), - "words32" => { - let mut result = String::new(); - for (i, chunk) in data.chunks(4).enumerate() { - if chunk.len() == 4 { - let addr = base_address + (i * 4) as u64; - let word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - result.push_str(&format!("0x{:08X}: 0x{:08X}\n", addr, word)); - } - } - result - } - "words16" => { - let mut result = String::new(); - for (i, chunk) in data.chunks(2).enumerate() { - if chunk.len() == 2 { - let addr = base_address + (i * 2) as u64; - let word = u16::from_le_bytes([chunk[0], chunk[1]]); - result.push_str(&format!("0x{:08X}: 0x{:04X}\n", addr, word)); - } - } - result - } - "ascii" => String::from_utf8_lossy(data).to_string(), - _ => { - // Default to hex if unknown format - format_memory_data(data, "hex", base_address) - } - } -} - -#[tool_handler] -impl ServerHandler for EmbeddedDebuggerToolHandler { - fn get_info(&self) -> ServerInfo { - ServerInfo { - protocol_version: ProtocolVersion::V_2024_11_05, - capabilities: ServerCapabilities::builder().enable_tools().build(), - server_info: Implementation::from_build_env(), - instructions: Some("Embedded debugging and flash programming MCP server for ARM Cortex-M, RISC-V, and other probe-rs-supported targets. Exposes 22 tools for probe detection, target sessions, memory operations, breakpoints, RTT communication, and flash programming: list_probes, connect, disconnect, probe_info, halt, run, reset, step, get_status, read_memory, write_memory, set_breakpoint, clear_breakpoint, rtt_attach, rtt_detach, rtt_read, rtt_write, rtt_channels, flash_erase, flash_program, flash_verify, run_firmware.".to_string()), - } - } - - async fn initialize( - &self, - _request: InitializeRequestParam, - _context: RequestContext, - ) -> Result { - info!("Embedded Debugger MCP server initialized with 22 tools (18 debug + 4 flash)"); - Ok(self.get_info()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn flash_program_rejects_when_erase_permission_disabled() { - let handler = EmbeddedDebuggerToolHandler::new(Config::default()); - let result = handler - .flash_program(Parameters(FlashProgramArgs { - session_id: "missing-session".to_string(), - file_path: "firmware.elf".to_string(), - format: "auto".to_string(), - base_address: None, - verify: true, - })) - .await; - - let error = format!("{:?}", result.expect_err("flash program must fail closed")); - assert!(error.contains("Flash erase is disabled")); - } - - #[test] - fn restricted_rtt_exact_address_uses_target_memory_map() { - let mut config = Config::default(); - config.security.restrict_memory_access = true; - let handler = EmbeddedDebuggerToolHandler::new(config); - - assert!(handler - .prepare_rtt_scan_region("STM32F407VGTx", Some(0x2000_0000), None) - .is_ok()); - assert!(handler - .prepare_rtt_scan_region("STM32F407VGTx", Some(0x4000_0000), None) - .is_err()); - } - - #[test] - fn restricted_rtt_ranges_must_stay_inside_readable_regions() { - let mut config = Config::default(); - config.security.restrict_memory_access = true; - let handler = EmbeddedDebuggerToolHandler::new(config); - - assert!(handler - .prepare_rtt_scan_region( - "STM32F407VGTx", - None, - Some(vec![(0x2000_0000, 0x2000_0100)]) - ) - .is_ok()); - assert!(handler - .prepare_rtt_scan_region( - "STM32F407VGTx", - None, - Some(vec![(0x2002_FF00, 0x2003_0100)]) - ) - .is_err()); - assert!(handler - .prepare_rtt_scan_region( - "STM32F407VGTx", - None, - Some(vec![(0x2000_0000, 0x2000_0000)]) - ) - .is_err()); - } -} +//! The tool router is split by domain while keeping one exported handler type for RMCP. + +mod flash; +mod formatting; +mod guards; +mod management; +mod memory; +mod rtt; +mod server; +mod session; +mod target_control; + +pub use session::{DebugSession, EmbeddedDebuggerToolHandler}; diff --git a/src/tools/debugger_tools/flash.rs b/src/tools/debugger_tools/flash.rs new file mode 100644 index 0000000..80cbc64 --- /dev/null +++ b/src/tools/debugger_tools/flash.rs @@ -0,0 +1,652 @@ +use rmcp::{handler::server::tool::Parameters, model::*, tool, tool_router, ErrorData as McpError}; +use std::future::Future; +use tracing::{debug, error, info, warn}; + +use super::formatting::{parse_address, parse_data}; +use super::session::EmbeddedDebuggerToolHandler; +use crate::tools::types::*; + +#[tool_router(router = flash_tool_router, vis = "pub")] +impl EmbeddedDebuggerToolHandler { + // ============================================================================= + // Flash Programming Tools (4 tools) + // ============================================================================= + + #[tool(description = "Erase flash memory sectors or entire chip")] + async fn flash_erase( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Flash erase for session: {}, type: {}", + args.session_id, args.erase_type + ); + + self.ensure_flash_erase_allowed()?; + let session_arc = self.get_session(&args.session_id).await?; + + // Parse erase type and parameters + let erase_type = match args.erase_type.as_str() { + "all" => crate::flash::EraseType::All, + "sectors" => { + let address = match args.address { + Some(addr_str) => { + parse_address(&addr_str).map_err(|e| McpError::internal_error(e, None))? + } + None => { + return Err(McpError::internal_error( + "Address required for sector erase".to_string(), + None, + )) + } + }; + let size = match args.size { + Some(sz) => sz as usize, + None => { + return Err(McpError::internal_error( + "Size required for sector erase".to_string(), + None, + )) + } + }; + crate::flash::EraseType::Sectors { address, size } + } + _ => { + return Err(McpError::internal_error( + format!("Invalid erase type: {}", args.erase_type), + None, + )) + } + }; + + // Perform erase operation + { + let mut session = session_arc.session.lock().await; + match crate::flash::FlashManager::erase_flash(&mut session, erase_type).await { + Ok(result) => { + let message = format!( + "Flash erase completed successfully.\n\n\ + Session ID: {}\n\ + Erase Type: {}\n\ + Duration: {}ms\n\ + {}\n\n\ + Flash memory has been erased and is ready for programming.", + args.session_id, + args.erase_type, + result.erase_time_ms, + match result.sectors_erased { + Some(count) => format!("Sectors Erased: {}", count), + None => "Full chip erased".to_string(), + } + ); + + info!("Flash erase completed for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!("Flash erase failed for session {}: {}", args.session_id, e); + let error_msg = format!( + "Flash erase failed\n\n\ + Session ID: {}\n\ + Error: {}\n\n\ + Suggestions:\n\ + - Check if flash is write-protected\n\ + - Ensure target is halted\n\ + - Verify flash address range", + args.session_id, e + ); + Err(McpError::internal_error(error_msg, None)) + } + } + } + } + + #[tool(description = "Program file to flash memory (supports ELF, HEX, BIN)")] + async fn flash_program( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Flash program for session: {}, file: {}", + args.session_id, args.file_path + ); + + self.ensure_flash_erase_allowed()?; + + let session_arc = self.get_session(&args.session_id).await?; + + // Parse file path and format + let file_path = + self.resolve_allowed_file_path(&args.file_path, self.config.flash.max_binary_size)?; + let format = match args.format.as_str() { + "auto" => crate::flash::FileFormat::Auto, + "elf" => crate::flash::FileFormat::Elf, + "hex" => crate::flash::FileFormat::Hex, + "bin" => crate::flash::FileFormat::Bin, + _ => { + return Err(McpError::internal_error( + format!("Unsupported format: {}", args.format), + None, + )) + } + }; + + // Parse base address if provided + let base_address = if let Some(addr_str) = args.base_address { + Some(parse_address(&addr_str).map_err(|e| McpError::internal_error(e, None))?) + } else { + None + }; + + // Perform programming operation + { + let mut session = session_arc.session.lock().await; + let verify = args.verify || self.config.flash.verify_after_program; + match crate::flash::FlashManager::program_file( + &mut session, + &file_path, + format, + base_address, + verify, + ) + .await + { + Ok(result) => { + let message = format!( + "Flash programming completed successfully.\n\n\ + Session ID: {}\n\ + File: {}\n\ + Format: {}\n\ + Bytes Programmed: {}\n\ + Duration: {}ms\n\ + Verification: {}\n\n\ + Firmware has been programmed to flash memory.", + args.session_id, + file_path.display(), + args.format, + result.bytes_programmed, + result.programming_time_ms, + match result.verification_result { + Some(true) => "Passed", + Some(false) => "Failed", + None => "Not performed", + } + ); + + info!( + "Flash programming completed for session: {}", + args.session_id + ); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!( + "Flash programming failed for session {}: {}", + args.session_id, e + ); + let error_msg = format!( + "Flash programming failed\n\n\ + Session ID: {}\n\ + File: {}\n\ + Error: {}\n\n\ + Suggestions:\n\ + - Check file exists and is readable\n\ + - Verify file format is correct\n\ + - Ensure flash is erased first\n\ + - Check target memory map", + args.session_id, args.file_path, e + ); + Err(McpError::internal_error(error_msg, None)) + } + } + } + } + + #[tool(description = "Verify flash memory contents")] + async fn flash_verify( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!("Flash verify for session: {}", args.session_id); + + let session_arc = { + let sessions = self.sessions.read().await; + match sessions.get(&args.session_id) { + Some(session) => session.clone(), + None => { + let error_msg = format!("Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); + return Err(McpError::internal_error(error_msg, None)); + } + } + }; + + // Parse address + let address = + parse_address(&args.address).map_err(|e| McpError::internal_error(e, None))?; + + // Get expected data + let expected_data = if let Some(file_path) = &args.file_path { + let file_path = + self.resolve_allowed_file_path(file_path, self.config.flash.max_binary_size)?; + let extension = file_path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_ascii_lowercase()); + if matches!(extension.as_deref(), Some("elf" | "hex")) { + return Err(McpError::internal_error( + format!( + "flash_verify compares raw bytes only; '{}' must be verified with a raw BIN file or hex data.", + file_path.display() + ), + None, + )); + } + std::fs::read(&file_path).map_err(|e| { + McpError::internal_error( + format!("Failed to read file {}: {}", file_path.display(), e), + None, + ) + })? + } else if let Some(hex_data) = &args.data { + // Parse hex data + match parse_data(hex_data, "hex") { + Ok(data) => data, + Err(e) => { + return Err(McpError::internal_error( + format!("Invalid hex data: {}", e), + None, + )) + } + } + } else { + return Err(McpError::internal_error( + "Either file_path or data must be provided".to_string(), + None, + )); + }; + + let verify_size = args.size as usize; + if expected_data.len() < verify_size { + return Err(McpError::internal_error( + format!( + "Expected data has {} bytes, fewer than requested verify size {}.", + expected_data.len(), + verify_size + ), + None, + )); + } + + let expected_data = if expected_data.len() > verify_size { + &expected_data[..verify_size] + } else { + &expected_data + }; + + // Perform verification + { + let mut session = session_arc.session.lock().await; + match crate::flash::FlashManager::verify_flash(&mut session, expected_data, address) + .await + { + Ok(result) => { + let message = if result.success { + format!( + "Flash verification successful.\n\n\ + Session ID: {}\n\ + Address: 0x{:08X}\n\ + Bytes Verified: {}\n\n\ + All flash contents match expected data.", + args.session_id, address, result.bytes_verified + ) + } else { + let mut message = format!( + "Flash verification failed.\n\n\ + Session ID: {}\n\ + Address: 0x{:08X}\n\ + Bytes Verified: {}\n\ + Mismatches: {}\n\n\ + First {} mismatches:\n", + args.session_id, + address, + result.bytes_verified, + result.mismatches.len(), + std::cmp::min(10, result.mismatches.len()) + ); + + for (i, mismatch) in result.mismatches.iter().take(10).enumerate() { + message.push_str(&format!( + " {}. 0x{:08X}: expected 0x{:02X}, got 0x{:02X}\n", + i + 1, + mismatch.address, + mismatch.expected, + mismatch.actual + )); + } + + if result.mismatches.len() > 10 { + message.push_str(&format!( + " ... and {} more mismatches\n", + result.mismatches.len() - 10 + )); + } + + message + }; + + info!( + "Flash verification completed for session: {}", + args.session_id + ); + if result.success { + Ok(CallToolResult::success(vec![Content::text(message)])) + } else { + Ok(CallToolResult::error(vec![Content::text(message)])) + } + } + Err(e) => { + error!( + "Flash verification failed for session {}: {}", + args.session_id, e + ); + let error_msg = format!( + "Flash verification error\n\n\ + Session ID: {}\n\ + Error: {}", + args.session_id, e + ); + Err(McpError::internal_error(error_msg, None)) + } + } + } + } + + #[tool(description = "Firmware deployment helper: erase, program, verify, run, and attach RTT")] + async fn run_firmware( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Run firmware for session: {}, file: {}", + args.session_id, args.file_path + ); + + self.ensure_flash_erase_allowed()?; + let session_arc = self.get_session(&args.session_id).await?; + let file_path = + self.resolve_allowed_file_path(&args.file_path, self.config.flash.max_binary_size)?; + + let mut status_messages = Vec::new(); + let start_time = std::time::Instant::now(); + + // Step 1: Erase flash + status_messages.push("Step 1/5: Erasing flash memory...".to_string()); + { + let mut session = session_arc.session.lock().await; + match crate::flash::FlashManager::erase_flash( + &mut session, + crate::flash::EraseType::All, + ) + .await + { + Ok(_) => status_messages.push("Flash erased successfully".to_string()), + Err(e) => { + let error_msg = format!("Flash erase failed: {}", e); + status_messages.push(error_msg.clone()); + return Err(McpError::internal_error( + format!("{}\n\n{}", status_messages.join("\n"), error_msg), + None, + )); + } + } + } + + // Step 2: Program firmware + status_messages.push("Step 2/5: Programming firmware...".to_string()); + let format = match args.format.as_str() { + "auto" => crate::flash::FileFormat::Auto, + "elf" => crate::flash::FileFormat::Elf, + "hex" => crate::flash::FileFormat::Hex, + "bin" => crate::flash::FileFormat::Bin, + _ => { + return Err(McpError::internal_error( + format!("Unsupported format: {}", args.format), + None, + )) + } + }; + + { + let mut session = session_arc.session.lock().await; + match crate::flash::FlashManager::program_file( + &mut session, + &file_path, + format, + None, + self.config.flash.verify_after_program, + ) + .await + { + Ok(result) => { + status_messages.push(format!("Programmed {} bytes", result.bytes_programmed)) + } + Err(e) => { + let error_msg = format!("Programming failed: {}", e); + status_messages.push(error_msg.clone()); + return Err(McpError::internal_error( + format!("{}\n\n{}", status_messages.join("\n"), error_msg), + None, + )); + } + } + } + + // Step 3: Reset and run + if args.reset_after_flash { + status_messages.push("Step 3/5: Resetting target...".to_string()); + { + let mut session = session_arc.session.lock().await; + let mut core = match session.core(0) { + Ok(core) => core, + Err(e) => { + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )) + } + }; + + match core.reset() { + Ok(_) => { + status_messages.push("Target reset successfully".to_string()); + // Run the target + match core.run() { + Ok(_) => status_messages.push("Target running".to_string()), + Err(e) => { + let error_msg = format!("Run after reset failed: {}", e); + status_messages.push(error_msg.clone()); + return Err(McpError::internal_error( + format!("{}\n\n{}", status_messages.join("\n"), error_msg), + None, + )); + } + } + } + Err(e) => { + let error_msg = format!("Reset failed: {}", e); + status_messages.push(error_msg.clone()); + return Err(McpError::internal_error( + format!("{}\n\n{}", status_messages.join("\n"), error_msg), + None, + )); + } + } + } + } + + // Step 4: Attach RTT (if requested) - Mimic probe-rs run behavior + if args.attach_rtt { + status_messages.push("Step 4/5: Attaching RTT...".to_string()); + + let timeout = tokio::time::Duration::from_millis(args.rtt_timeout_ms as u64); + let started = tokio::time::Instant::now(); + let mut rtt_attached = false; + let mut last_rtt_error = None; + let mut attempt = 1; + + loop { + if attempt > 1 { + let remaining = timeout.saturating_sub(started.elapsed()); + if remaining.is_zero() { + break; + } + tokio::time::sleep(remaining.min(tokio::time::Duration::from_millis(500))) + .await; + } + + // Try RTT attachment with different strategies (probe-rs style optimization) + let mut rtt_manager = session_arc.rtt_manager.lock().await; + let rtt_result = match attempt { + 1..=2 => { + // First 2 attempts: ELF symbol detection (probe-rs priority method) + debug!( + "RTT attempt {}: Using ELF symbol detection (probe-rs style)", + attempt + ); + rtt_manager + .attach_with_elf(session_arc.session.clone(), &file_path) + .await + } + 3..=5 => { + // Attempts 3-5: standard attach, let probe-rs auto-scan memory + debug!("RTT attempt {}: Using standard memory map scan", attempt); + rtt_manager + .attach(session_arc.session.clone(), None, None) + .await + } + 6..=7 => { + // Attempts 6-7: try STM32G4 specific memory ranges + debug!( + "RTT attempt {}: Using STM32G4 specific memory ranges", + attempt + ); + let stm32g4_ranges = vec![ + (0x20000000, 0x20004000), // SRAM1 first half: 16KB - most likely RTT location + (0x20004000, 0x20008000), // SRAM1 second half: 16KB + (0x20008000, 0x2000A000), // SRAM2: 8KB + ]; + rtt_manager + .attach(session_arc.session.clone(), None, Some(stm32g4_ranges)) + .await + } + _ => { + // Last attempt: try common RTT control block addresses + let cb_addr = 0x20000000; + debug!( + "RTT attempt {}: Using specific control block address 0x{:08X}", + attempt, cb_addr + ); + rtt_manager + .attach(session_arc.session.clone(), Some(cb_addr), None) + .await + } + }; + + match rtt_result { + Ok(_) => { + let up_channels = rtt_manager.up_channel_count(); + let down_channels = rtt_manager.down_channel_count(); + status_messages.push(format!( + "RTT attached on attempt {} ({} up, {} down channels)", + attempt, up_channels, down_channels + )); + info!("RTT successfully attached after {} attempts!", attempt); + rtt_attached = true; + break; + } + Err(e) => { + debug!("RTT attach attempt {} failed: {}", attempt, e); + last_rtt_error = Some(e.to_string()); + } + } + + if args.rtt_timeout_ms == 0 || started.elapsed() >= timeout { + break; + } + attempt += 1; + } + + if !rtt_attached { + let error_msg = format!( + "RTT attach failed within {}ms after {} attempt(s): {}", + args.rtt_timeout_ms, + attempt, + last_rtt_error.unwrap_or_else(|| "timeout expired".to_string()) + ); + status_messages.push(error_msg.clone()); + warn!("{}", error_msg); + return Err(McpError::internal_error( + format!("{}\n\n{}", status_messages.join("\n"), error_msg), + None, + )); + } + + info!("RTT connected successfully, allowing channel stabilization..."); + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + } + + status_messages.push("Step 5/5: Finalizing...".to_string()); + let elapsed = start_time.elapsed(); + + let message = format!( + "Firmware deployment completed.\n\n\ + Session ID: {}\n\ + File: {}\n\ + Format: {}\n\ + Total Time: {:.1}s\n\n\ + Status:\n{}\n\n\ + Firmware is now running on target.\n\ + {}", + args.session_id, + file_path.display(), + args.format, + elapsed.as_secs_f64(), + status_messages.join("\n"), + if args.attach_rtt { + "Use 'rtt_read' to monitor target output." + } else { + "Use 'rtt_attach' to enable real-time communication." + } + ); + + info!( + "Firmware deployment completed for session: {} in {:.1}s", + args.session_id, + elapsed.as_secs_f64() + ); + Ok(CallToolResult::success(vec![Content::text(message)])) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + + #[tokio::test] + async fn flash_program_rejects_when_erase_permission_disabled() { + let handler = EmbeddedDebuggerToolHandler::new(Config::default()); + let result = handler + .flash_program(Parameters(FlashProgramArgs { + session_id: "missing-session".to_string(), + file_path: "firmware.elf".to_string(), + format: "auto".to_string(), + base_address: None, + verify: true, + })) + .await; + + let error = format!("{:?}", result.expect_err("flash program must fail closed")); + assert!(error.contains("Flash erase is disabled")); + } +} diff --git a/src/tools/debugger_tools/formatting.rs b/src/tools/debugger_tools/formatting.rs new file mode 100644 index 0000000..37264d9 --- /dev/null +++ b/src/tools/debugger_tools/formatting.rs @@ -0,0 +1,157 @@ +// ============================================================================= +// Utility Functions +// ============================================================================= + +/// Parse address string (hex or decimal) to u64 +pub(super) fn parse_address(addr_str: &str) -> Result { + let addr_str = addr_str.trim(); + + if addr_str.starts_with("0x") || addr_str.starts_with("0X") { + u64::from_str_radix(&addr_str[2..], 16).map_err(|e| format!("Invalid hex address: {}", e)) + } else { + addr_str + .parse::() + .map_err(|e| format!("Invalid decimal address: {}", e)) + } +} + +/// Parse data string based on format +pub(super) fn parse_data(data_str: &str, format: &str) -> Result, String> { + match format { + "hex" => { + // Remove spaces and 0x prefixes + let clean_str = data_str + .replace(" ", "") + .replace("0x", "") + .replace("0X", ""); + if (clean_str.len() & 1) != 0 { + return Err("Hex data must have even number of characters".to_string()); + } + + (0..clean_str.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&clean_str[i..i + 2], 16)) + .collect::, _>>() + .map_err(|e| format!("Invalid hex data: {}", e)) + } + "ascii" => Ok(data_str.as_bytes().to_vec()), + "words32" => { + let words: Result, _> = data_str + .split_whitespace() + .map(|s| { + if s.starts_with("0x") || s.starts_with("0X") { + u32::from_str_radix(&s[2..], 16) + } else { + s.parse::() + } + }) + .collect(); + + match words { + Ok(words) => { + let mut data = Vec::new(); + for word in words { + data.extend_from_slice(&word.to_le_bytes()); + } + Ok(data) + } + Err(e) => Err(format!("Invalid word32 data: {}", e)), + } + } + "words16" => { + let words: Result, _> = data_str + .split_whitespace() + .map(|s| { + if s.starts_with("0x") || s.starts_with("0X") { + u16::from_str_radix(&s[2..], 16) + } else { + s.parse::() + } + }) + .collect(); + + match words { + Ok(words) => { + let mut data = Vec::new(); + for word in words { + data.extend_from_slice(&word.to_le_bytes()); + } + Ok(data) + } + Err(e) => Err(format!("Invalid word16 data: {}", e)), + } + } + _ => Err(format!("Unsupported data format: {}", format)), + } +} + +/// Format memory data for display +pub(super) fn format_memory_data(data: &[u8], format: &str, base_address: u64) -> String { + match format { + "hex" => { + let mut result = String::new(); + for (i, chunk) in data.chunks(16).enumerate() { + let addr = base_address + (i * 16) as u64; + result.push_str(&format!("0x{:08X}: ", addr)); + + // Hex bytes + for (j, byte) in chunk.iter().enumerate() { + if j == 8 { + result.push(' '); + } + result.push_str(&format!("{:02X} ", byte)); + } + + // Pad if needed + if chunk.len() < 16 { + let padding = (16 - chunk.len()) * 3 + (if chunk.len() <= 8 { 1 } else { 0 }); + result.push_str(&" ".repeat(padding)); + } + + // ASCII representation + result.push_str("| "); + for byte in chunk { + if byte.is_ascii_graphic() || *byte == b' ' { + result.push(*byte as char); + } else { + result.push('.'); + } + } + result.push('\n'); + } + result + } + "binary" => data + .iter() + .map(|b| format!("{:08b}", b)) + .collect::>() + .join(" "), + "words32" => { + let mut result = String::new(); + for (i, chunk) in data.chunks(4).enumerate() { + if chunk.len() == 4 { + let addr = base_address + (i * 4) as u64; + let word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + result.push_str(&format!("0x{:08X}: 0x{:08X}\n", addr, word)); + } + } + result + } + "words16" => { + let mut result = String::new(); + for (i, chunk) in data.chunks(2).enumerate() { + if chunk.len() == 2 { + let addr = base_address + (i * 2) as u64; + let word = u16::from_le_bytes([chunk[0], chunk[1]]); + result.push_str(&format!("0x{:08X}: 0x{:04X}\n", addr, word)); + } + } + result + } + "ascii" => String::from_utf8_lossy(data).to_string(), + _ => { + // Default to hex if unknown format + format_memory_data(data, "hex", base_address) + } + } +} diff --git a/src/tools/debugger_tools/guards.rs b/src/tools/debugger_tools/guards.rs new file mode 100644 index 0000000..ee8caf4 --- /dev/null +++ b/src/tools/debugger_tools/guards.rs @@ -0,0 +1,391 @@ +use rmcp::ErrorData as McpError; +use std::path::{Path, PathBuf}; + +use super::session::{DebugSession, EmbeddedDebuggerToolHandler}; +use crate::config::TargetConfig; + +const RTT_CONTROL_BLOCK_HEADER_BYTES: usize = 16; +pub(super) type RttScanRange = (u64, u64); +pub(super) type PreparedRttScan = (Option, Option>); + +impl EmbeddedDebuggerToolHandler { + pub(super) fn flash_erase_allowed(&self) -> bool { + self.config.security.allow_flash_erase || self.config.flash.allow_erase + } + + pub(super) fn ensure_flash_erase_allowed(&self) -> Result<(), McpError> { + if self.flash_erase_allowed() { + Ok(()) + } else { + Err(McpError::internal_error( + "Flash erase is disabled by configuration. Enable security.allow_flash_erase or flash.allow_erase to use this operation." + .to_string(), + None, + )) + } + } + + pub(super) fn ensure_memory_read_allowed( + &self, + session: &DebugSession, + address: u64, + size: usize, + ) -> Result<(), McpError> { + if size == 0 { + return Err(McpError::internal_error( + "Memory read size must be greater than zero.".to_string(), + None, + )); + } + if size > self.config.memory.max_read_size { + return Err(McpError::internal_error( + format!( + "Memory read size {} exceeds configured limit {}.", + size, self.config.memory.max_read_size + ), + None, + )); + } + self.ensure_memory_region_allowed(session, address, size, 'r') + } + + pub(super) fn ensure_memory_write_allowed( + &self, + session: &DebugSession, + address: u64, + size: usize, + ) -> Result<(), McpError> { + if !self.config.security.allow_memory_write { + return Err(McpError::internal_error( + "Memory writes are disabled by configuration.".to_string(), + None, + )); + } + if size == 0 { + return Err(McpError::internal_error( + "Memory write size must be greater than zero.".to_string(), + None, + )); + } + if size > self.config.memory.max_write_size { + return Err(McpError::internal_error( + format!( + "Memory write size {} exceeds configured limit {}.", + size, self.config.memory.max_write_size + ), + None, + )); + } + self.ensure_memory_region_allowed(session, address, size, 'w') + } + + pub(super) fn ensure_memory_region_allowed( + &self, + session: &DebugSession, + address: u64, + size: usize, + required_access: char, + ) -> Result<(), McpError> { + self.ensure_memory_region_allowed_for_target( + &session.target_chip, + address, + size, + required_access, + ) + } + + pub(super) fn ensure_memory_region_allowed_for_target( + &self, + target_chip: &str, + address: u64, + size: usize, + required_access: char, + ) -> Result<(), McpError> { + let end_exclusive = address.checked_add(size as u64).ok_or_else(|| { + McpError::internal_error( + "Memory range overflows u64 address space.".to_string(), + None, + ) + })?; + + if !self.config.security.restrict_memory_access { + return Ok(()); + } + + let target = self.target_config_for(target_chip).ok_or_else(|| { + McpError::internal_error( + format!( + "Memory access is restricted, but target '{}' has no configured memory map.", + target_chip + ), + None, + ) + })?; + + let last_address = end_exclusive - 1; + let allowed = target.memory_regions.iter().any(|region| { + address >= region.start + && last_address <= region.end + && region.access.contains(required_access) + }); + + if allowed { + Ok(()) + } else { + Err(McpError::internal_error( + format!( + "Memory range 0x{address:08X}..0x{last_address:08X} is outside configured '{}' access regions for target '{}'.", + required_access, target_chip + ), + None, + )) + } + } + + pub(super) fn prepare_rtt_scan_region( + &self, + target_chip: &str, + control_block_address: Option, + memory_ranges: Option>, + ) -> Result { + let control_block_address = control_block_address.or(self.config.rtt.control_block_address); + + if let Some(address) = control_block_address { + self.ensure_memory_region_allowed_for_target( + target_chip, + address, + RTT_CONTROL_BLOCK_HEADER_BYTES, + 'r', + )?; + return Ok((Some(address), None)); + } + + if let Some(ranges) = memory_ranges { + self.ensure_rtt_ranges_allowed(target_chip, &ranges)?; + return Ok((None, Some(ranges))); + } + + if self.config.security.restrict_memory_access { + return Ok((None, Some(self.configured_rtt_scan_ranges(target_chip)?))); + } + + Ok((None, None)) + } + + pub(super) fn ensure_rtt_ranges_allowed( + &self, + target_chip: &str, + ranges: &[RttScanRange], + ) -> Result<(), McpError> { + if ranges.is_empty() { + return Err(McpError::internal_error( + "RTT memory ranges must not be empty.".to_string(), + None, + )); + } + + for (start, end) in ranges { + let size = end.checked_sub(*start).ok_or_else(|| { + McpError::internal_error( + format!( + "RTT memory range 0x{start:08X}..0x{end:08X} has an invalid end address." + ), + None, + ) + })?; + if size == 0 { + return Err(McpError::internal_error( + format!("RTT memory range 0x{start:08X}..0x{end:08X} is empty."), + None, + )); + } + let size = usize::try_from(size).map_err(|_| { + McpError::internal_error( + format!("RTT memory range 0x{start:08X}..0x{end:08X} is too large."), + None, + ) + })?; + self.ensure_memory_region_allowed_for_target(target_chip, *start, size, 'r')?; + } + + Ok(()) + } + + pub(super) fn configured_rtt_scan_ranges( + &self, + target_chip: &str, + ) -> Result, McpError> { + let target = self.target_config_for(target_chip).ok_or_else(|| { + McpError::internal_error( + format!( + "RTT scan is restricted, but target '{}' has no configured memory map.", + target_chip + ), + None, + ) + })?; + + let ranges: Result, _> = target + .memory_regions + .iter() + .filter(|region| region.access.contains('r')) + .filter(|region| { + !self.config.rtt.scan_ram_only || region.name.to_ascii_lowercase().contains("ram") + }) + .map(|region| { + let end_exclusive = region.end.checked_add(1).ok_or_else(|| { + McpError::internal_error( + format!("Memory region '{}' end address overflows.", region.name), + None, + ) + })?; + Ok((region.start, end_exclusive)) + }) + .collect(); + + let ranges = ranges?; + if ranges.is_empty() { + return Err(McpError::internal_error( + format!( + "RTT scan is restricted, but target '{}' has no readable configured ranges.", + target_chip + ), + None, + )); + } + + Ok(ranges) + } + + pub(super) fn target_config_for(&self, target_chip: &str) -> Option<&TargetConfig> { + let target_chip_lower = target_chip.to_lowercase(); + self.config + .targets + .get(&target_chip_lower) + .or_else(|| self.config.targets.get(target_chip)) + .or_else(|| { + self.config.targets.values().find(|target| { + target.chip.eq_ignore_ascii_case(target_chip) + || target.name.eq_ignore_ascii_case(target_chip) + }) + }) + } + + pub(super) fn resolve_allowed_file_path( + &self, + path: &str, + max_size: usize, + ) -> Result { + let path = Path::new(path); + let canonical = path.canonicalize().map_err(|e| { + McpError::internal_error( + format!("Failed to resolve file path '{}': {}", path.display(), e), + None, + ) + })?; + + let metadata = canonical.metadata().map_err(|e| { + McpError::internal_error( + format!( + "Failed to read metadata for '{}': {}", + canonical.display(), + e + ), + None, + ) + })?; + if !metadata.is_file() { + return Err(McpError::internal_error( + format!("Path '{}' is not a regular file.", canonical.display()), + None, + )); + } + + let file_size = metadata.len() as usize; + let max_size = max_size.min(self.config.security.max_file_size); + if file_size > max_size { + return Err(McpError::internal_error( + format!( + "File '{}' is {} bytes, exceeding configured limit {}.", + canonical.display(), + file_size, + max_size + ), + None, + )); + } + + if self.config.security.allowed_file_paths.is_empty() { + return Ok(canonical); + } + + let allowed = self + .config + .security + .allowed_file_paths + .iter() + .filter_map(|root| Path::new(root).canonicalize().ok()) + .any(|root| canonical.starts_with(root)); + + if allowed { + Ok(canonical) + } else { + Err(McpError::internal_error( + format!( + "File '{}' is outside configured allowed_file_paths.", + canonical.display() + ), + None, + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + + #[test] + fn restricted_rtt_exact_address_uses_target_memory_map() { + let mut config = Config::default(); + config.security.restrict_memory_access = true; + let handler = EmbeddedDebuggerToolHandler::new(config); + + assert!(handler + .prepare_rtt_scan_region("STM32F407VGTx", Some(0x2000_0000), None) + .is_ok()); + assert!(handler + .prepare_rtt_scan_region("STM32F407VGTx", Some(0x4000_0000), None) + .is_err()); + } + + #[test] + fn restricted_rtt_ranges_must_stay_inside_readable_regions() { + let mut config = Config::default(); + config.security.restrict_memory_access = true; + let handler = EmbeddedDebuggerToolHandler::new(config); + + assert!(handler + .prepare_rtt_scan_region( + "STM32F407VGTx", + None, + Some(vec![(0x2000_0000, 0x2000_0100)]) + ) + .is_ok()); + assert!(handler + .prepare_rtt_scan_region( + "STM32F407VGTx", + None, + Some(vec![(0x2002_FF00, 0x2003_0100)]) + ) + .is_err()); + assert!(handler + .prepare_rtt_scan_region( + "STM32F407VGTx", + None, + Some(vec![(0x2000_0000, 0x2000_0000)]) + ) + .is_err()); + } +} diff --git a/src/tools/debugger_tools/management.rs b/src/tools/debugger_tools/management.rs new file mode 100644 index 0000000..1a693f0 --- /dev/null +++ b/src/tools/debugger_tools/management.rs @@ -0,0 +1,320 @@ +use rmcp::{handler::server::tool::Parameters, model::*, tool, tool_router, ErrorData as McpError}; +use std::future::Future; +use std::sync::Arc; +use tracing::{debug, error, info}; + +use super::session::{DebugSession, EmbeddedDebuggerToolHandler}; +use crate::rtt::RttManager; +use crate::tools::types::*; +use probe_rs::{probe::list::Lister, Permissions}; + +#[tool_router(router = management_tool_router, vis = "pub")] +impl EmbeddedDebuggerToolHandler { + // ============================================================================= + // Debugger Management Tools (4 tools) + // ============================================================================= + + #[tool(description = "List all available debug probes (J-Link, ST-Link, DAPLink, etc.)")] + async fn list_probes( + &self, + Parameters(_args): Parameters, + ) -> Result { + debug!("Listing available debug probes"); + + // Real probe-rs integration + let probes = Lister::new().list_all(); + let message = if probes.is_empty() { + "No debug probes found.\n\nPlease ensure your probe is connected and drivers are installed.\nSupported probes: J-Link, ST-Link, DAPLink, Black Magic Probe".to_string() + } else { + let mut result = format!("Found {} debug probe(s):\n\n", probes.len()); + + for (i, probe) in probes.iter().enumerate() { + result.push_str(&format!("{}. {}\n", i + 1, probe.identifier)); + result.push_str(&format!( + " VID:PID = {:04X}:{:04X}\n", + probe.vendor_id, probe.product_id + )); + + if let Some(serial) = &probe.serial_number { + result.push_str(&format!(" Serial: {}\n", serial)); + } + + result.push_str(&format!(" Probe Type: {:?}\n", probe.probe_type())); + result.push('\n'); + } + + result + }; + + info!("Listed {} debug probes", probes.len()); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + + #[tool(description = "Connect to a debug probe and target chip")] + async fn connect( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Connecting to probe '{}' and target '{}'", + args.probe_selector, args.target_chip + ); + + let session_slot = self + .session_slots + .clone() + .try_acquire_owned() + .map_err(|_| { + McpError::internal_error( + format!( + "Session limit exceeded. Maximum {} sessions allowed.", + self.max_sessions + ), + None, + ) + })?; + + // Real probe-rs implementation + let probes = Lister::new().list_all(); + + if probes.is_empty() { + return Err(McpError::internal_error( + "No debug probes found. Please connect a supported probe (J-Link, ST-Link, DAPLink, etc.)".to_string(), + None + )); + } + + let selected_probe = if args.probe_selector.to_lowercase() == "auto" { + probes.first() + } else { + probes + .iter() + .find(|p| p.identifier.contains(&args.probe_selector)) + }; + + match selected_probe { + Some(probe_info) => { + info!("Opening probe: {}", probe_info.identifier); + match probe_info.open() { + Ok(mut probe) => { + let actual_speed = probe.set_speed(args.speed_khz).map_err(|e| { + McpError::internal_error( + format!( + "Failed to set probe speed to {} kHz: {}", + args.speed_khz, e + ), + None, + ) + })?; + + let permissions = if self.flash_erase_allowed() { + Permissions::new().allow_erase_all() + } else { + Permissions::new() + }; + + let connect_under_reset = + args.connect_under_reset || self.config.debugger.connect_under_reset; + let halt_after_connect = + args.halt_after_connect || self.config.debugger.halt_on_connect; + + info!("Attaching to target: {}", args.target_chip); + let attach_result = if connect_under_reset { + probe.attach_under_reset(&args.target_chip, permissions) + } else { + probe.attach(&args.target_chip, permissions) + }; + + match attach_result { + Ok(mut session) => { + if halt_after_connect { + let mut core = session.core(0).map_err(|e| { + McpError::internal_error( + format!( + "Connected but failed to get core for halt: {}", + e + ), + None, + ) + })?; + core.halt(std::time::Duration::from_millis( + self.config.debugger.connection_timeout_ms, + )) + .map_err(|e| { + McpError::internal_error( + format!("Connected but failed to halt target: {}", e), + None, + ) + })?; + } + + let session_id = format!("session_{}", uuid::Uuid::new_v4()); + + let debug_session = DebugSession { + session_id: session_id.clone(), + probe_identifier: probe_info.identifier.clone(), + target_chip: args.target_chip.clone(), + created_at: chrono::Utc::now(), + session: Arc::new(tokio::sync::Mutex::new(session)), + rtt_manager: Arc::new(tokio::sync::Mutex::new( + RttManager::new(), + )), + _session_slot: session_slot, + }; + + // Store session + { + let mut sessions = self.sessions.write().await; + sessions.insert(session_id.clone(), Arc::new(debug_session)); + } + + let message = format!( + "Debug session established.\n\n\ + Session ID: {}\n\ + Probe: {} (VID:PID = {:04X}:{:04X})\n\ + Target: {}\n\ + Speed: {} kHz\n\ + Connect under reset: {}\n\ + Halted after connect: {}\n\ + Connected at: {}\n\n\ + Target connection established and ready for debugging.\n\ + Use this session ID for all debug operations.", + session_id, + probe_info.identifier, + probe_info.vendor_id, + probe_info.product_id, + args.target_chip, + actual_speed, + connect_under_reset, + halt_after_connect, + chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC") + ); + + info!("Created debug session: {}", session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!("Failed to attach to target '{}': {}", args.target_chip, e); + let error_msg = format!( + "Failed to attach to target '{}'\n\n\ + Error: {}\n\n\ + Suggestions:\n\ + - Check target chip name (try: STM32F407VGTx, nRF52840_xxAA)\n\ + - Ensure target is powered and connected\n\ + - Verify SWD/JTAG connections", + args.target_chip, e + ); + Err(McpError::internal_error(error_msg, None)) + } + } + } + Err(e) => { + error!("Failed to open probe '{}': {}", probe_info.identifier, e); + let error_msg = format!( + "Failed to open probe '{}'\n\nError: {}\n\n\ + Suggestions:\n\ + - Check probe drivers installation\n\ + - Verify USB connection\n\ + - Try disconnecting and reconnecting probe", + probe_info.identifier, e + ); + Err(McpError::internal_error(error_msg, None)) + } + } + } + None => { + let available_probes: Vec = probes + .iter() + .map(|p| format!("- {}", p.identifier)) + .collect(); + + let error_msg = format!( + "Probe '{}' not found\n\n\ + Available probes:\n{}\n\n\ + Use 'auto' to connect to first available probe.", + args.probe_selector, + available_probes.join("\n") + ); + Err(McpError::internal_error(error_msg, None)) + } + } + } + + #[tool(description = "Disconnect from a debug session")] + async fn disconnect( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!("Disconnecting session: {}", args.session_id); + + // Remove session from storage + let removed_session = { + let mut sessions = self.sessions.write().await; + sessions.remove(&args.session_id) + }; + + match removed_session { + Some(session) => { + let message = format!( + "Debug session disconnected successfully\n\n\ + Session ID: {}\n\ + Probe: {}\n\ + Target: {}\n\ + Duration: {:.1} minutes\n\n\ + probe-rs Session resources have been cleaned up.", + args.session_id, + session.probe_identifier, + session.target_chip, + (chrono::Utc::now() - session.created_at).num_seconds() as f64 / 60.0 + ); + + info!("Disconnected debug session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + None => { + let error_msg = format!( + "Session '{}' not found\n\nUse 'connect' to establish a debug session first", + args.session_id + ); + Err(McpError::internal_error(error_msg, None)) + } + } + } + + #[tool(description = "Get basic information about a debug session")] + async fn probe_info( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!("Getting probe info for session: {}", args.session_id); + + // Get session from storage + let session_arc = self.get_session(&args.session_id).await?; + + // Calculate session duration + let duration_minutes = + (chrono::Utc::now() - session_arc.created_at).num_seconds() as f64 / 60.0; + + let message = format!( + "Debug Session Information\n\n\ + Probe Information:\n\ + - Identifier: {}\n\ + - Connected: true\n\n\ + Target Information:\n\ + - Chip: {}\n\n\ + Session Status:\n\ + - Session ID: {}\n\ + - Created: {}\n\ + - Duration: {:.1} minutes\n\n\ + Session is active and ready for operations.", + session_arc.probe_identifier, + session_arc.target_chip, + args.session_id, + session_arc.created_at.format("%Y-%m-%d %H:%M:%S UTC"), + duration_minutes + ); + + info!("Retrieved probe info for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) + } +} diff --git a/src/tools/debugger_tools/memory.rs b/src/tools/debugger_tools/memory.rs new file mode 100644 index 0000000..1a8262b --- /dev/null +++ b/src/tools/debugger_tools/memory.rs @@ -0,0 +1,331 @@ +use rmcp::{handler::server::tool::Parameters, model::*, tool, tool_router, ErrorData as McpError}; +use std::future::Future; +use tracing::{debug, error, info}; + +use super::formatting::{format_memory_data, parse_address, parse_data}; +use super::session::EmbeddedDebuggerToolHandler; +use crate::tools::types::*; +use probe_rs::MemoryInterface; + +#[tool_router(router = memory_tool_router, vis = "pub")] +impl EmbeddedDebuggerToolHandler { + // ============================================================================= + // Memory Operation Tools (2 tools) + // ============================================================================= + + #[tool(description = "Read memory from the target")] + async fn read_memory( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Reading memory for session: {} at address {}", + args.session_id, args.address + ); + + // Parse address + let address = match parse_address(&args.address) { + Ok(addr) => addr, + Err(e) => { + error!("Invalid address '{}': {}", args.address, e); + return Err(McpError::internal_error( + format!("Invalid address '{}': {}", args.address, e), + None, + )); + } + }; + + let session_arc = self.get_session(&args.session_id).await?; + self.ensure_memory_read_allowed(&session_arc, address, args.size)?; + + // Read memory + { + let mut session = session_arc.session.lock().await; + let mut core = match session.core(0) { + Ok(core) => core, + Err(e) => { + error!("Failed to get core for session {}: {}", args.session_id, e); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); + } + }; + + let mut data = vec![0u8; args.size]; + match core.read(address, &mut data) { + Ok(_) => { + debug!("Read {} bytes from address 0x{:08X}", data.len(), address); + + let formatted_data = format_memory_data(&data, &args.format, address); + let message = format!( + "Memory read completed successfully.\n\n\ + Session ID: {}\n\ + Address: 0x{:08X}\n\ + Size: {} bytes\n\ + Format: {}\n\n\ + Data:\n{}", + args.session_id, address, args.size, args.format, formatted_data + ); + + info!("Memory read completed for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!( + "Failed to read memory for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to read memory: {}", e), + None, + )) + } + } + } + } + + #[tool(description = "Write memory to the target")] + async fn write_memory( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Writing memory for session: {} at address {}", + args.session_id, args.address + ); + + // Parse address + let address = match parse_address(&args.address) { + Ok(addr) => addr, + Err(e) => { + error!("Invalid address '{}': {}", args.address, e); + return Err(McpError::internal_error( + format!("Invalid address '{}': {}", args.address, e), + None, + )); + } + }; + + // Parse data based on format + let data = match parse_data(&args.data, &args.format) { + Ok(data) => data, + Err(e) => { + error!("Invalid data '{}': {}", args.data, e); + return Err(McpError::internal_error( + format!("Invalid data '{}': {}", args.data, e), + None, + )); + } + }; + + let session_arc = self.get_session(&args.session_id).await?; + self.ensure_memory_write_allowed(&session_arc, address, data.len())?; + + // Write memory + { + let mut session = session_arc.session.lock().await; + let mut core = match session.core(0) { + Ok(core) => core, + Err(e) => { + error!("Failed to get core for session {}: {}", args.session_id, e); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); + } + }; + + match core.write(address, &data) { + Ok(_) => { + let message = format!( + "Memory write completed successfully.\n\n\ + Session ID: {}\n\ + Address: 0x{:08X}\n\ + Data: {}\n\ + Format: {}\n\ + Bytes written: {}", + args.session_id, + address, + args.data, + args.format, + data.len() + ); + + info!("Memory write completed for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!( + "Failed to write memory for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to write memory: {}", e), + None, + )) + } + } + } + } + + // ============================================================================= + // Breakpoint Tools (2 tools) + // ============================================================================= + + #[tool(description = "Set a breakpoint at the specified address")] + async fn set_breakpoint( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Setting breakpoint for session: {} at address {}", + args.session_id, args.address + ); + + if args.breakpoint_type != "hardware" { + return Err(McpError::internal_error( + format!( + "Unsupported breakpoint_type '{}'. Only hardware breakpoints are implemented.", + args.breakpoint_type + ), + None, + )); + } + + // Parse address + let address = match parse_address(&args.address) { + Ok(addr) => addr, + Err(e) => { + error!("Invalid address '{}': {}", args.address, e); + return Err(McpError::internal_error( + format!("Invalid address '{}': {}", args.address, e), + None, + )); + } + }; + + let session_arc = self.get_session(&args.session_id).await?; + + // Set breakpoint + { + let mut session = session_arc.session.lock().await; + let mut core = match session.core(0) { + Ok(core) => core, + Err(e) => { + error!("Failed to get core for session {}: {}", args.session_id, e); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); + } + }; + + match core.set_hw_breakpoint(address) { + Ok(_) => { + let message = format!( + "Breakpoint set successfully.\n\n\ + Session ID: {}\n\ + Address: 0x{:08X}\n\ + Type: Hardware breakpoint\n\n\ + The target will halt when execution reaches this address.", + args.session_id, address + ); + + info!( + "Breakpoint set for session: {} at 0x{:08X}", + args.session_id, address + ); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!( + "Failed to set breakpoint for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to set breakpoint: {}", e), + None, + )) + } + } + } + } + + #[tool(description = "Clear a breakpoint at the specified address")] + async fn clear_breakpoint( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Clearing breakpoint for session: {} at address {}", + args.session_id, args.address + ); + + // Parse address + let address = match parse_address(&args.address) { + Ok(addr) => addr, + Err(e) => { + error!("Invalid address '{}': {}", args.address, e); + return Err(McpError::internal_error( + format!("Invalid address '{}': {}", args.address, e), + None, + )); + } + }; + + let session_arc = { + let sessions = self.sessions.read().await; + match sessions.get(&args.session_id) { + Some(session) => session.clone(), + None => { + let error_msg = format!("Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); + return Err(McpError::internal_error(error_msg, None)); + } + } + }; + + // Clear breakpoint + { + let mut session = session_arc.session.lock().await; + let mut core = match session.core(0) { + Ok(core) => core, + Err(e) => { + error!("Failed to get core for session {}: {}", args.session_id, e); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); + } + }; + + match core.clear_hw_breakpoint(address) { + Ok(_) => { + let message = format!( + "Breakpoint cleared successfully.\n\n\ + Session ID: {}\n\ + Address: 0x{:08X}\n\n\ + The breakpoint has been removed.", + args.session_id, address + ); + + info!( + "Breakpoint cleared for session: {} at 0x{:08X}", + args.session_id, address + ); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!( + "Failed to clear breakpoint for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to clear breakpoint: {}", e), + None, + )) + } + } + } + } +} diff --git a/src/tools/debugger_tools/rtt.rs b/src/tools/debugger_tools/rtt.rs new file mode 100644 index 0000000..3cb327d --- /dev/null +++ b/src/tools/debugger_tools/rtt.rs @@ -0,0 +1,439 @@ +use rmcp::{handler::server::tool::Parameters, model::*, tool, tool_router, ErrorData as McpError}; +use std::future::Future; +use tracing::{debug, error, info}; + +use super::formatting::parse_address; +use super::session::EmbeddedDebuggerToolHandler; +use crate::tools::types::*; + +#[tool_router(router = rtt_tool_router, vis = "pub")] +impl EmbeddedDebuggerToolHandler { + // ============================================================================= + // RTT Communication Tools (5 tools) + // ============================================================================= + + #[tool(description = "Attach to RTT (Real-Time Transfer) for communication with target")] + async fn rtt_attach( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!("Attaching RTT for session: {}", args.session_id); + + let session_arc = self.get_session(&args.session_id).await?; + + // Parse control block address if provided + let control_block_address = if let Some(addr_str) = args.control_block_address { + match parse_address(&addr_str) { + Ok(addr) => Some(addr), + Err(e) => { + let error_msg = format!("Invalid control block address '{}': {}", addr_str, e); + return Err(McpError::internal_error(error_msg, None)); + } + } + } else { + None + }; + + // Parse memory ranges if provided + let memory_ranges = if let Some(ranges) = args.memory_ranges { + let mut parsed_ranges = Vec::new(); + for range in ranges { + let start = parse_address(&range.start).map_err(|e| { + McpError::internal_error( + format!("Invalid start address '{}': {}", range.start, e), + None, + ) + })?; + let end = parse_address(&range.end).map_err(|e| { + McpError::internal_error( + format!("Invalid end address '{}': {}", range.end, e), + None, + ) + })?; + parsed_ranges.push((start, end)); + } + Some(parsed_ranges) + } else { + None + }; + + let (control_block_address, memory_ranges) = self.prepare_rtt_scan_region( + &session_arc.target_chip, + control_block_address, + memory_ranges, + )?; + + // Attach RTT + { + let mut rtt_manager = session_arc.rtt_manager.lock().await; + match rtt_manager + .attach( + session_arc.session.clone(), + control_block_address, + memory_ranges, + ) + .await + { + Ok(_) => { + let up_channels = rtt_manager.up_channel_count(); + let down_channels = rtt_manager.down_channel_count(); + + let message = format!( + "RTT attached successfully!\n\n\ + Session ID: {}\n\ + Up Channels (Target to Host): {}\n\ + Down Channels (Host to Target): {}\n\n\ + RTT is now ready for real-time communication with the target.\n\ + Use 'rtt_read' to read from target and 'rtt_write' to send data to target.", + args.session_id, up_channels, down_channels + ); + + info!("RTT attached successfully for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!( + "Failed to attach RTT for session {}: {}", + args.session_id, e + ); + let error_msg = format!( + "Failed to attach RTT\n\n\ + Session ID: {}\n\ + Error: {}\n\n\ + Suggestions:\n\ + - Ensure the target firmware has RTT enabled and initialized\n\ + - Check that the target is halted\n\ + - Verify memory ranges if specified\n\ + - Try different control block address if known", + args.session_id, e + ); + Err(McpError::internal_error(error_msg, None)) + } + } + } + } + + #[tool(description = "Detach from RTT communication")] + async fn rtt_detach( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!("Detaching RTT for session: {}", args.session_id); + + let session_arc = self.get_session(&args.session_id).await?; + + // Detach RTT + { + let mut rtt_manager = session_arc.rtt_manager.lock().await; + match rtt_manager.detach().await { + Ok(_) => { + let message = format!( + "RTT detached successfully\n\n\ + Session ID: {}\n\n\ + RTT communication has been closed.", + args.session_id + ); + + info!("RTT detached successfully for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!( + "Failed to detach RTT for session {}: {}", + args.session_id, e + ); + let error_msg = format!("Failed to detach RTT: {}", e); + Err(McpError::internal_error(error_msg, None)) + } + } + } + } + + #[tool(description = "Read data from RTT up channel (target to host)")] + async fn rtt_read( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Reading from RTT channel {} for session: {}", + args.channel, args.session_id + ); + + let session_arc = self.get_session(&args.session_id).await?; + if args.max_bytes == 0 { + return Err(McpError::internal_error( + "max_bytes must be greater than zero.".to_string(), + None, + )); + } + if args.max_bytes > self.config.rtt.buffer_size { + return Err(McpError::internal_error( + format!( + "max_bytes {} exceeds configured RTT buffer size {}.", + args.max_bytes, self.config.rtt.buffer_size + ), + None, + )); + } + + // Read from RTT + { + let mut rtt_manager = session_arc.rtt_manager.lock().await; + if !rtt_manager.is_attached() { + let error_msg = format!( + "RTT not attached for session '{}'\n\nUse 'rtt_attach' first", + args.session_id + ); + return Err(McpError::internal_error(error_msg, None)); + } + + match rtt_manager + .read_channel(args.channel, args.max_bytes, args.timeout_ms) + .await + { + Ok(data) => { + let data_len = data.len(); + let data_str = if data.is_empty() { + "No data available".to_string() + } else { + // Try to decode as UTF-8, fall back to hex if not valid + match String::from_utf8(data.clone()) { + Ok(text) => { + if text + .chars() + .all(|c| c.is_ascii_graphic() || c.is_ascii_whitespace()) + { + format!("Text: {}", text) + } else { + format!("Mixed: {} (hex: {})", text, hex::encode(&data)) + } + } + Err(_) => format!("Binary data (hex): {}", hex::encode(&data)), + } + }; + + let message = format!( + "RTT Read from Channel {}\n\n\ + Session ID: {}\n\ + Bytes Read: {}\n\n\ + Data:\n{}", + args.channel, args.session_id, data_len, data_str + ); + + debug!( + "Read {} bytes from RTT channel {} for session: {}", + data_len, args.channel, args.session_id + ); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!( + "Failed to read from RTT channel {} for session {}: {}", + args.channel, args.session_id, e + ); + let error_msg = format!( + "Failed to read from RTT channel {}\n\n\ + Session ID: {}\n\ + Error: {}", + args.channel, args.session_id, e + ); + Err(McpError::internal_error(error_msg, None)) + } + } + } + } + + #[tool(description = "Write data to RTT down channel (host to target)")] + async fn rtt_write( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!( + "Writing to RTT channel {} for session: {}", + args.channel, args.session_id + ); + + // Get session from storage + let session_arc = { + let sessions = self.sessions.read().await; + match sessions.get(&args.session_id) { + Some(session) => session.clone(), + None => { + let error_msg = format!("Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); + return Err(McpError::internal_error(error_msg, None)); + } + } + }; + + // Parse data based on encoding + let data_bytes = match args.encoding.as_str() { + "utf8" => args.data.as_bytes().to_vec(), + "hex" => match hex::decode(&args.data) { + Ok(bytes) => bytes, + Err(e) => { + let error_msg = format!("Invalid hex data '{}': {}", args.data, e); + return Err(McpError::internal_error(error_msg, None)); + } + }, + "binary" => { + // Parse binary string like "10110011 11001100" + let binary_str = args.data.replace(' ', ""); + if binary_str.len() % 8 != 0 { + let error_msg = + format!("Binary data must be multiple of 8 bits: '{}'", args.data); + return Err(McpError::internal_error(error_msg, None)); + } + + let mut bytes = Vec::new(); + for chunk in binary_str.chars().collect::>().chunks(8) { + let byte_str: String = chunk.iter().collect(); + match u8::from_str_radix(&byte_str, 2) { + Ok(byte) => bytes.push(byte), + Err(e) => { + let error_msg = format!("Invalid binary byte '{}': {}", byte_str, e); + return Err(McpError::internal_error(error_msg, None)); + } + } + } + bytes + } + _ => { + let error_msg = format!( + "Unsupported encoding '{}'. Use 'utf8', 'hex', or 'binary'", + args.encoding + ); + return Err(McpError::internal_error(error_msg, None)); + } + }; + + // Write to RTT + { + let mut rtt_manager = session_arc.rtt_manager.lock().await; + if !rtt_manager.is_attached() { + let error_msg = format!( + "RTT not attached for session '{}'\n\nUse 'rtt_attach' first", + args.session_id + ); + return Err(McpError::internal_error(error_msg, None)); + } + + match rtt_manager.write_channel(args.channel, &data_bytes).await { + Ok(bytes_written) => { + let message = format!( + "RTT Write to Channel {}\n\n\ + Session ID: {}\n\ + Data: {}\n\ + Encoding: {}\n\ + Bytes Written: {}\n\n\ + Data sent successfully to target.", + args.channel, args.session_id, args.data, args.encoding, bytes_written + ); + + info!( + "Wrote {} bytes to RTT channel {} for session: {}", + bytes_written, args.channel, args.session_id + ); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!( + "Failed to write to RTT channel {} for session {}: {}", + args.channel, args.session_id, e + ); + let error_msg = format!( + "Failed to write to RTT channel {}\n\n\ + Session ID: {}\n\ + Error: {}", + args.channel, args.session_id, e + ); + Err(McpError::internal_error(error_msg, None)) + } + } + } + } + + #[tool(description = "List available RTT channels")] + async fn rtt_channels( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!("Listing RTT channels for session: {}", args.session_id); + + // Get session from storage + let session_arc = { + let sessions = self.sessions.read().await; + match sessions.get(&args.session_id) { + Some(session) => session.clone(), + None => { + let error_msg = format!("Session '{}' not found\n\nUse 'connect' to establish a debug session first", args.session_id); + return Err(McpError::internal_error(error_msg, None)); + } + } + }; + + // List RTT channels + { + let rtt_manager = session_arc.rtt_manager.lock().await; + if !rtt_manager.is_attached() { + let error_msg = format!( + "RTT not attached for session '{}'\n\nUse 'rtt_attach' first", + args.session_id + ); + return Err(McpError::internal_error(error_msg, None)); + } + + let channels = rtt_manager.get_channels(); + let channel_count = channels.len(); + + if channels.is_empty() { + let message = format!( + "📋 RTT Channels\n\n\ + Session ID: {}\n\n\ + No RTT channels available.", + args.session_id + ); + return Ok(CallToolResult::success(vec![Content::text(message)])); + } + + let mut message = format!("📋 RTT Channels\n\nSession ID: {}\n\n", args.session_id); + + // Group channels by direction + let mut up_channels = Vec::new(); + let mut down_channels = Vec::new(); + + for channel in &channels { + match channel.direction { + crate::rtt::ChannelDirection::Up => up_channels.push(channel), + crate::rtt::ChannelDirection::Down => down_channels.push(channel), + } + } + + if !up_channels.is_empty() { + message.push_str("Up Channels (Target to Host):\n"); + for channel in up_channels { + message.push_str(&format!( + " {}. {} (Size: {} bytes, Mode: {})\n", + channel.id, channel.name, channel.buffer_size, channel.mode + )); + } + message.push('\n'); + } + + if !down_channels.is_empty() { + message.push_str("Down Channels (Host to Target):\n"); + for channel in down_channels { + message.push_str(&format!( + " {}. {} (Size: {} bytes, Mode: {})\n", + channel.id, channel.name, channel.buffer_size, channel.mode + )); + } + } + + info!( + "Listed {} RTT channels for session: {}", + channel_count, args.session_id + ); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + } +} diff --git a/src/tools/debugger_tools/server.rs b/src/tools/debugger_tools/server.rs new file mode 100644 index 0000000..15164dc --- /dev/null +++ b/src/tools/debugger_tools/server.rs @@ -0,0 +1,28 @@ +use rmcp::{ + model::*, service::RequestContext, tool_handler, ErrorData as McpError, RoleServer, + ServerHandler, +}; +use tracing::info; + +use super::session::EmbeddedDebuggerToolHandler; + +#[tool_handler] +impl ServerHandler for EmbeddedDebuggerToolHandler { + fn get_info(&self) -> ServerInfo { + ServerInfo { + protocol_version: ProtocolVersion::V_2024_11_05, + capabilities: ServerCapabilities::builder().enable_tools().build(), + server_info: Implementation::from_build_env(), + instructions: Some("Embedded debugging and flash programming MCP server for ARM Cortex-M, RISC-V, and other probe-rs-supported targets. Exposes 22 tools for probe detection, target sessions, memory operations, breakpoints, RTT communication, and flash programming: list_probes, connect, disconnect, probe_info, halt, run, reset, step, get_status, read_memory, write_memory, set_breakpoint, clear_breakpoint, rtt_attach, rtt_detach, rtt_read, rtt_write, rtt_channels, flash_erase, flash_program, flash_verify, run_firmware.".to_string()), + } + } + + async fn initialize( + &self, + _request: InitializeRequestParam, + _context: RequestContext, + ) -> Result { + info!("Embedded Debugger MCP server initialized with 22 tools (18 debug + 4 flash)"); + Ok(self.get_info()) + } +} diff --git a/src/tools/debugger_tools/session.rs b/src/tools/debugger_tools/session.rs new file mode 100644 index 0000000..7d3ba51 --- /dev/null +++ b/src/tools/debugger_tools/session.rs @@ -0,0 +1,113 @@ +use rmcp::{handler::server::router::tool::ToolRouter, ErrorData as McpError}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::{OwnedSemaphorePermit, RwLock, Semaphore}; + +use crate::config::Config; +use crate::rtt::RttManager; +use probe_rs::Session; + +/// Debug session information +pub struct DebugSession { + pub session_id: String, + pub probe_identifier: String, + pub target_chip: String, + pub created_at: chrono::DateTime, + pub session: Arc>, + pub rtt_manager: Arc>, + pub(super) _session_slot: OwnedSemaphorePermit, +} + +/// Embedded debugger tool handler with debug, RTT, and flash tools +#[derive(Clone)] +pub struct EmbeddedDebuggerToolHandler { + #[allow(dead_code)] + pub(crate) tool_router: ToolRouter, + pub(crate) sessions: Arc>>>, + pub(crate) config: Arc, + pub(crate) max_sessions: usize, + pub(crate) session_slots: Arc, +} + +impl EmbeddedDebuggerToolHandler { + pub fn new(config: impl Into) -> Self { + let config = config.into(); + let max_sessions = config.server.max_sessions; + Self { + tool_router: Self::management_tool_router() + + Self::target_control_tool_router() + + Self::memory_tool_router() + + Self::rtt_tool_router() + + Self::flash_tool_router(), + sessions: Arc::new(RwLock::new(HashMap::new())), + config: Arc::new(config), + max_sessions, + session_slots: Arc::new(Semaphore::new(max_sessions)), + } + } + + pub(super) async fn get_session( + &self, + session_id: &str, + ) -> Result, McpError> { + let sessions = self.sessions.read().await; + sessions.get(session_id).cloned().ok_or_else(|| { + McpError::internal_error( + format!( + "Session '{}' not found. Use 'connect' to establish a debug session first.", + session_id + ), + None, + ) + }) + } +} + +impl Default for EmbeddedDebuggerToolHandler { + fn default() -> Self { + Self::new(5) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn combined_router_registers_all_tools() { + let handler = EmbeddedDebuggerToolHandler::default(); + let tools = handler.tool_router.list_all(); + let names = tools + .iter() + .map(|tool| tool.name.as_ref()) + .collect::>(); + + assert_eq!(tools.len(), 22); + for expected in [ + "list_probes", + "connect", + "disconnect", + "probe_info", + "halt", + "run", + "reset", + "step", + "get_status", + "read_memory", + "write_memory", + "set_breakpoint", + "clear_breakpoint", + "rtt_attach", + "rtt_detach", + "rtt_read", + "rtt_write", + "rtt_channels", + "flash_erase", + "flash_program", + "flash_verify", + "run_firmware", + ] { + assert!(names.contains(expected), "missing tool: {expected}"); + } + } +} diff --git a/src/tools/debugger_tools/target_control.rs b/src/tools/debugger_tools/target_control.rs new file mode 100644 index 0000000..03a36ac --- /dev/null +++ b/src/tools/debugger_tools/target_control.rs @@ -0,0 +1,366 @@ +use rmcp::{handler::server::tool::Parameters, model::*, tool, tool_router, ErrorData as McpError}; +use std::future::Future; +use tracing::{debug, error, info, warn}; + +use super::session::EmbeddedDebuggerToolHandler; +use crate::tools::types::*; +use probe_rs::{CoreStatus, RegisterValue}; + +#[tool_router(router = target_control_tool_router, vis = "pub")] +impl EmbeddedDebuggerToolHandler { + // ============================================================================= + // Target Control Tools (5 tools) + // ============================================================================= + + #[tool(description = "Halt the target CPU execution")] + async fn halt( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!("Halting target for session: {}", args.session_id); + + let session_arc = self.get_session(&args.session_id).await?; + + // Halt the target + { + let mut session = session_arc.session.lock().await; + let mut core = match session.core(0) { + Ok(core) => core, + Err(e) => { + error!("Failed to get core for session {}: {}", args.session_id, e); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); + } + }; + + match core.halt(std::time::Duration::from_millis(1000)) { + Ok(_) => { + // Get status after halt + match core.status() { + Ok(_status) => { + let pc = core + .read_core_reg(core.program_counter()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + let sp = core + .read_core_reg(core.stack_pointer()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + + let message = format!( + "Target halted successfully!\n\n\ + Session ID: {}\n\ + PC: 0x{:08X}\n\ + SP: 0x{:08X}\n\ + State: Halted\n", + args.session_id, pc, sp + ); + + info!("Halt completed for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + warn!("Failed to get status after halt: {}", e); + let message = format!( + "Target halted successfully!\n\n\ + Session ID: {}\n\ + State: Halted\n", + args.session_id + ); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + } + } + Err(e) => { + error!( + "Failed to halt target for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to halt target: {}", e), + None, + )) + } + } + } + } + + #[tool(description = "Resume target CPU execution")] + async fn run(&self, Parameters(args): Parameters) -> Result { + debug!("Running target for session: {}", args.session_id); + + let session_arc = self.get_session(&args.session_id).await?; + + // Resume the target + { + let mut session = session_arc.session.lock().await; + let mut core = match session.core(0) { + Ok(core) => core, + Err(e) => { + error!("Failed to get core for session {}: {}", args.session_id, e); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); + } + }; + + match core.run() { + Ok(_) => { + let message = format!( + "Target resumed execution successfully!\n\n\ + Session ID: {}\n\ + Status: Running\n\n\ + The target is now executing code. Use 'halt' to stop execution.", + args.session_id + ); + + info!("Run completed for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!( + "Failed to run target for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to run target: {}", e), + None, + )) + } + } + } + } + + #[tool(description = "Reset the target CPU")] + async fn reset( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!("Resetting target for session: {}", args.session_id); + + if args.reset_type != "hardware" { + return Err(McpError::internal_error( + format!( + "Unsupported reset_type '{}'. probe-rs core reset is exposed as 'hardware' by this server.", + args.reset_type + ), + None, + )); + } + + let session_arc = self.get_session(&args.session_id).await?; + + // Reset the target + { + let mut session = session_arc.session.lock().await; + let mut core = match session.core(0) { + Ok(core) => core, + Err(e) => { + error!("Failed to get core for session {}: {}", args.session_id, e); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); + } + }; + + let reset_result = if args.halt_after_reset { + core.reset_and_halt(std::time::Duration::from_millis( + self.config.debugger.connection_timeout_ms, + )) + .map(|_| ()) + } else { + core.reset() + }; + + match reset_result { + Ok(_) => { + let pc = core + .read_core_reg(core.program_counter()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + let sp = core + .read_core_reg(core.stack_pointer()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + + let message = format!( + "Target reset completed successfully.\n\n\ + Session ID: {}\n\ + Reset type: {}\n\ + Halted after reset: {}\n\ + PC: 0x{:08X}\n\ + SP: 0x{:08X}\n\ + State: {}\n", + args.session_id, + args.reset_type, + args.halt_after_reset, + pc, + sp, + if args.halt_after_reset { + "Halted" + } else { + "Running" + } + ); + + info!("Reset completed for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!( + "Failed to reset target for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to reset target: {}", e), + None, + )) + } + } + } + } + + #[tool(description = "Execute a single instruction step")] + async fn step( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!("Single stepping target for session: {}", args.session_id); + + let session_arc = self.get_session(&args.session_id).await?; + + // Single step the target + { + let mut session = session_arc.session.lock().await; + let mut core = match session.core(0) { + Ok(core) => core, + Err(e) => { + error!("Failed to get core for session {}: {}", args.session_id, e); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); + } + }; + + match core.step() { + Ok(_) => { + let pc = core + .read_core_reg(core.program_counter()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + let sp = core + .read_core_reg(core.stack_pointer()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + + let message = format!( + "Single step completed successfully!\n\n\ + Session ID: {}\n\ + PC: 0x{:08X}\n\ + SP: 0x{:08X}\n\ + State: Halted\n", + args.session_id, pc, sp + ); + + info!("Step completed for session: {}", args.session_id); + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!( + "Failed to step target for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to step target: {}", e), + None, + )) + } + } + } + } + + #[tool(description = "Get current status of the target CPU and debug session")] + async fn get_status( + &self, + Parameters(args): Parameters, + ) -> Result { + debug!("Getting status for session: {}", args.session_id); + + let session_arc = self.get_session(&args.session_id).await?; + + // Get target status + { + let mut session = session_arc.session.lock().await; + let mut core = match session.core(0) { + Ok(core) => core, + Err(e) => { + error!("Failed to get core for session {}: {}", args.session_id, e); + return Err(McpError::internal_error( + format!("Failed to get core: {}", e), + None, + )); + } + }; + + match core.status() { + Ok(status) => { + let pc = core + .read_core_reg(core.program_counter()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + let sp = core + .read_core_reg(core.stack_pointer()) + .map(|v: RegisterValue| v.try_into().unwrap_or(0u32)) + .unwrap_or(0); + + let is_halted = matches!(status, CoreStatus::Halted(_)); + let halt_reason = match status { + CoreStatus::Halted(reason) => format!("{:?}", reason), + CoreStatus::Running => "N/A".to_string(), + _ => "Unknown".to_string(), + }; + + let message = format!( + "Debug Session Status\n\n\ + Core Information:\n\ + - PC: 0x{:08X}\n\ + - SP: 0x{:08X}\n\ + - State: {}\n\ + - Halt reason: {}\n\n\ + Session Information:\n\ + - ID: {}\n\ + - Connected: true\n\ + - Target: {}\n\ + - Probe: {}\n\ + - Duration: {:.1} minutes\n", + pc, + sp, + if is_halted { "Halted" } else { "Running" }, + halt_reason, + args.session_id, + session_arc.target_chip, + session_arc.probe_identifier, + (chrono::Utc::now() - session_arc.created_at).num_seconds() as f64 / 60.0 + ); + + Ok(CallToolResult::success(vec![Content::text(message)])) + } + Err(e) => { + error!( + "Failed to get core status for session {}: {}", + args.session_id, e + ); + Err(McpError::internal_error( + format!("Failed to get core status: {}", e), + None, + )) + } + } + } + } +} From 87396d3f4c205eeed15bb8d6828b66ffc57f961c Mon Sep 17 00:00:00 2001 From: Hongda Chen <54146249+Adancurusul@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:34:15 +0800 Subject: [PATCH 4/8] fix: clean debugger tool split drift --- src/tools/debugger_tools/rtt.rs | 4 ++-- src/tools/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tools/debugger_tools/rtt.rs b/src/tools/debugger_tools/rtt.rs index 3cb327d..1cb7d5b 100644 --- a/src/tools/debugger_tools/rtt.rs +++ b/src/tools/debugger_tools/rtt.rs @@ -387,7 +387,7 @@ impl EmbeddedDebuggerToolHandler { if channels.is_empty() { let message = format!( - "📋 RTT Channels\n\n\ + "RTT Channels\n\n\ Session ID: {}\n\n\ No RTT channels available.", args.session_id @@ -395,7 +395,7 @@ impl EmbeddedDebuggerToolHandler { return Ok(CallToolResult::success(vec![Content::text(message)])); } - let mut message = format!("📋 RTT Channels\n\nSession ID: {}\n\n", args.session_id); + let mut message = format!("RTT Channels\n\nSession ID: {}\n\n", args.session_id); // Group channels by direction let mut up_channels = Vec::new(); diff --git a/src/tools/mod.rs b/src/tools/mod.rs index ad0c061..dd56a61 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -7,6 +7,6 @@ pub mod debugger_tools; pub mod types; -// Export all 18 tools (13 base debugging + 5 RTT communication) +// Export all 22 tools (13 base debugging + 5 RTT communication + 4 flash tools) pub use debugger_tools::*; pub use types::*; From 4ef47f8fc70e57e1d855601c3055e39c1afb1e4a Mon Sep 17 00:00:00 2001 From: Hongda Chen <54146249+Adancurusul@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:58:21 +0800 Subject: [PATCH 5/8] docs: document cli skill install and claude plugin trigger --- .claude-plugin/plugin.json | 20 +++++++++++++++++++ README.md | 41 ++++++++++++++++++++++++++++++++++++-- README_zh.md | 39 ++++++++++++++++++++++++++++++++++-- 3 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 .claude-plugin/plugin.json diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..2ca8663 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,20 @@ +{ + "name": "embedded-debugger", + "version": "0.2.0", + "description": "Embedded debugger workflow for probe-rs targets. Provides a CLI-first skill and optional MCP server for probe discovery, target checks, flashing, memory access, and RTT workflows.", + "author": { + "name": "adancurusul", + "url": "https://github.com/Adancurusul" + }, + "repository": "https://github.com/Adancurusul/embedded-debugger-mcp", + "license": "MIT", + "keywords": [ + "embedded", + "debugging", + "mcp", + "probe-rs", + "rtt", + "flash", + "skill" + ] +} diff --git a/README.md b/README.md index 7ac03ac..43a4680 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,44 @@ The bundled skill lives in: skills/embedded-debugger/ ``` -It is written as a plain Codex/Claude Code skill. The skill starts with CLI -checks and uses MCP tools only when an MCP client is available. +It is written as a plain Codex skill and is also packaged for Claude Code with +`.claude-plugin/plugin.json`. The skill starts with CLI checks and uses MCP +tools only when an MCP client is available. + +Install the skill for Codex: + +```bash +mkdir -p ~/.codex/skills +cp -R skills/embedded-debugger ~/.codex/skills/ +``` + +Then trigger it with a prompt such as: + +```text +Use $embedded-debugger to inspect my embedded target setup. +``` + +Load it in Claude Code from this checkout: + +```bash +claude --plugin-dir . --print '/embedded-debugger inspect my embedded target setup' +``` + +For skill-only environments, the same `skills/embedded-debugger` directory can +also be copied to a local skills directory. The plugin-dir path above is the +validated Claude Code slash-command path for this repository. + +Validate the skill package: + +```bash +python3 .github/scripts/validate_skill.py skills/embedded-debugger +python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py skills/embedded-debugger +claude plugin validate . +``` + +The first command validates the repository skill metadata, the second validates +the standard `SKILL.md` layout when the Codex skill creator validator is +installed, and the third validates the Claude Code plugin manifest. ## MCP Tool Set @@ -199,6 +235,7 @@ cargo test --locked --all-targets --all-features RUSTDOCFLAGS="-D warnings" cargo doc --locked --all-features --no-deps cargo package --locked python3 .github/scripts/validate_skill.py skills/embedded-debugger +claude plugin validate . (cd examples/STM32_demo && CARGO_TARGET_DIR=/tmp/embedded-debugger-mcp-stm32-target cargo +nightly check --locked) ``` diff --git a/README_zh.md b/README_zh.md index 225ee6a..21f13e1 100644 --- a/README_zh.md +++ b/README_zh.md @@ -98,8 +98,42 @@ embedded-debugger-mcp skill print-prompt skills/embedded-debugger/ ``` -它是普通 Codex / Claude Code skill。工作流会先运行 CLI 检查;只有 MCP 客户端 -可用时,才会使用 MCP 工具做会话型调试操作。 +它是普通 Codex skill,同时通过 `.claude-plugin/plugin.json` 提供 Claude Code +插件加载方式。工作流会先运行 CLI 检查;只有 MCP 客户端可用时,才会使用 MCP +工具做会话型调试操作。 + +安装到 Codex: + +```bash +mkdir -p ~/.codex/skills +cp -R skills/embedded-debugger ~/.codex/skills/ +``` + +然后用类似这样的 prompt 触发: + +```text +Use $embedded-debugger to inspect my embedded target setup. +``` + +从当前 checkout 加载到 Claude Code: + +```bash +claude --plugin-dir . --print '/embedded-debugger inspect my embedded target setup' +``` + +对于只支持 skill 目录的环境,也可以直接复制 `skills/embedded-debugger` 到本地 +skills 目录。上面的 plugin-dir 方式是本仓库已验证的 Claude Code 斜杠命令路径。 + +验证 skill 包: + +```bash +python3 .github/scripts/validate_skill.py skills/embedded-debugger +python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py skills/embedded-debugger +claude plugin validate . +``` + +第一个命令验证仓库内 skill 元数据,第二个命令在已安装 Codex skill creator +validator 时验证标准 `SKILL.md` 布局,第三个命令验证 Claude Code 插件 manifest。 ## MCP 工具集 @@ -191,6 +225,7 @@ cargo test --locked --all-targets --all-features RUSTDOCFLAGS="-D warnings" cargo doc --locked --all-features --no-deps cargo package --locked python3 .github/scripts/validate_skill.py skills/embedded-debugger +claude plugin validate . (cd examples/STM32_demo && CARGO_TARGET_DIR=/tmp/embedded-debugger-mcp-stm32-target cargo +nightly check --locked) ``` From 3b790219ed01d0ce87345f54f8e742d1221cca1e Mon Sep 17 00:00:00 2001 From: Hongda Chen <54146249+Adancurusul@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:11:44 +0800 Subject: [PATCH 6/8] feat: add cli skill installer --- .github/workflows/ci.yml | 7 + README.md | 30 ++-- README_zh.md | 29 +++- src/config.rs | 35 ++++- src/main.rs | 57 ++++++-- src/skill_installer.rs | 287 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 421 insertions(+), 24 deletions(-) create mode 100644 src/skill_installer.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7a3a07..faa6617 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,13 @@ jobs: python3 -m json.tool /tmp/embedded-debugger-probes.json > /dev/null cargo run --locked -- skill print-prompt > /tmp/embedded-debugger-skill.txt grep -q "embedded-debugger skill" /tmp/embedded-debugger-skill.txt + cargo run --locked -- skill install --target both --home /tmp/embedded-debugger-skill-home --dry-run --json > /tmp/embedded-debugger-skill-install-dry-run.json + python3 -m json.tool /tmp/embedded-debugger-skill-install-dry-run.json > /dev/null + cargo run --locked -- skill install --target both --home /tmp/embedded-debugger-skill-home --force --json > /tmp/embedded-debugger-skill-install.json + python3 -m json.tool /tmp/embedded-debugger-skill-install.json > /dev/null + test -f /tmp/embedded-debugger-skill-home/.codex/skills/embedded-debugger/SKILL.md + test -f /tmp/embedded-debugger-skill-home/.claude/skills/embedded-debugger/SKILL.md + test -f /tmp/embedded-debugger-skill-home/.claude/plugins/local/embedded-debugger-mcp/.claude-plugin/plugin.json if cargo run --locked -- not-a-command > /tmp/embedded-debugger-invalid.out 2> /tmp/embedded-debugger-invalid.err; then echo "invalid subcommand unexpectedly succeeded" exit 1 diff --git a/README.md b/README.md index 43a4680..8d0a0d2 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ embedded-debugger-mcp config generate embedded-debugger-mcp config validate embedded-debugger-mcp config show embedded-debugger-mcp skill print-prompt +embedded-debugger-mcp skill install --target both ``` The bundled skill lives in: @@ -106,28 +107,39 @@ It is written as a plain Codex skill and is also packaged for Claude Code with `.claude-plugin/plugin.json`. The skill starts with CLI checks and uses MCP tools only when an MCP client is available. -Install the skill for Codex: +Install the skill for Codex and Claude Code: ```bash -mkdir -p ~/.codex/skills -cp -R skills/embedded-debugger ~/.codex/skills/ +embedded-debugger-mcp skill install --target both ``` -Then trigger it with a prompt such as: +Preview or automate the install with JSON: + +```bash +embedded-debugger-mcp skill install --target both --dry-run --json +embedded-debugger-mcp skill install --target both --force --json +``` + +`--target` accepts `codex`, `claude`, or `both`. The command installs the Codex +skill under `~/.codex/skills/embedded-debugger`, the Claude Code skill under +`~/.claude/skills/embedded-debugger`, and a local Claude plugin-dir package under +`~/.claude/plugins/local/embedded-debugger-mcp`. Existing directories are not +replaced unless `--force` is passed. + +Trigger the Codex skill with a prompt such as: ```text Use $embedded-debugger to inspect my embedded target setup. ``` -Load it in Claude Code from this checkout: +Load the installed Claude Code plugin package: ```bash -claude --plugin-dir . --print '/embedded-debugger inspect my embedded target setup' +claude --plugin-dir ~/.claude/plugins/local/embedded-debugger-mcp --print '/embedded-debugger inspect my embedded target setup' ``` For skill-only environments, the same `skills/embedded-debugger` directory can -also be copied to a local skills directory. The plugin-dir path above is the -validated Claude Code slash-command path for this repository. +also be copied to a local skills directory. Validate the skill package: @@ -135,6 +147,7 @@ Validate the skill package: python3 .github/scripts/validate_skill.py skills/embedded-debugger python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py skills/embedded-debugger claude plugin validate . +embedded-debugger-mcp skill install --target both --home /tmp/embedded-debugger-skill-home --force --json ``` The first command validates the repository skill metadata, the second validates @@ -236,6 +249,7 @@ RUSTDOCFLAGS="-D warnings" cargo doc --locked --all-features --no-deps cargo package --locked python3 .github/scripts/validate_skill.py skills/embedded-debugger claude plugin validate . +embedded-debugger-mcp skill install --target both --home /tmp/embedded-debugger-skill-home --force --json (cd examples/STM32_demo && CARGO_TARGET_DIR=/tmp/embedded-debugger-mcp-stm32-target cargo +nightly check --locked) ``` diff --git a/README_zh.md b/README_zh.md index 21f13e1..66aa92a 100644 --- a/README_zh.md +++ b/README_zh.md @@ -90,6 +90,7 @@ embedded-debugger-mcp config generate embedded-debugger-mcp config validate embedded-debugger-mcp config show embedded-debugger-mcp skill print-prompt +embedded-debugger-mcp skill install --target both ``` 内置 skill 位于: @@ -102,27 +103,39 @@ skills/embedded-debugger/ 插件加载方式。工作流会先运行 CLI 检查;只有 MCP 客户端可用时,才会使用 MCP 工具做会话型调试操作。 -安装到 Codex: +安装到 Codex 和 Claude Code: ```bash -mkdir -p ~/.codex/skills -cp -R skills/embedded-debugger ~/.codex/skills/ +embedded-debugger-mcp skill install --target both ``` -然后用类似这样的 prompt 触发: +可以先预览,也可以用 JSON 做自动化: + +```bash +embedded-debugger-mcp skill install --target both --dry-run --json +embedded-debugger-mcp skill install --target both --force --json +``` + +`--target` 支持 `codex`、`claude` 或 `both`。该命令会把 Codex skill 安装到 +`~/.codex/skills/embedded-debugger`,把 Claude Code skill 安装到 +`~/.claude/skills/embedded-debugger`,并把可通过 `--plugin-dir` 加载的 Claude +plugin 包安装到 `~/.claude/plugins/local/embedded-debugger-mcp`。已有目录默认不会 +覆盖,需要显式传入 `--force`。 + +Codex 里可以用类似这样的 prompt 触发: ```text Use $embedded-debugger to inspect my embedded target setup. ``` -从当前 checkout 加载到 Claude Code: +从已安装的 Claude Code plugin 包加载: ```bash -claude --plugin-dir . --print '/embedded-debugger inspect my embedded target setup' +claude --plugin-dir ~/.claude/plugins/local/embedded-debugger-mcp --print '/embedded-debugger inspect my embedded target setup' ``` 对于只支持 skill 目录的环境,也可以直接复制 `skills/embedded-debugger` 到本地 -skills 目录。上面的 plugin-dir 方式是本仓库已验证的 Claude Code 斜杠命令路径。 +skills 目录。 验证 skill 包: @@ -130,6 +143,7 @@ skills 目录。上面的 plugin-dir 方式是本仓库已验证的 Claude Code python3 .github/scripts/validate_skill.py skills/embedded-debugger python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py skills/embedded-debugger claude plugin validate . +embedded-debugger-mcp skill install --target both --home /tmp/embedded-debugger-skill-home --force --json ``` 第一个命令验证仓库内 skill 元数据,第二个命令在已安装 Codex skill creator @@ -226,6 +240,7 @@ RUSTDOCFLAGS="-D warnings" cargo doc --locked --all-features --no-deps cargo package --locked python3 .github/scripts/validate_skill.py skills/embedded-debugger claude plugin validate . +embedded-debugger-mcp skill install --target both --home /tmp/embedded-debugger-skill-home --force --json (cd examples/STM32_demo && CARGO_TARGET_DIR=/tmp/embedded-debugger-mcp-stm32-target cargo +nightly check --locked) ``` diff --git a/src/config.rs b/src/config.rs index cbcb4d7..5db92cb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ //! Configuration management for the debugger MCP server use crate::error::{DebugError, Result}; -use clap::{Parser, Subcommand}; +use clap::{Parser, Subcommand, ValueEnum}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; @@ -132,6 +132,39 @@ pub enum ProbeCommand { pub enum SkillCommand { /// Print the default prompt for CLI+Skill workflows. PrintPrompt, + /// Install the bundled skill for Codex and/or Claude Code. + Install { + /// Which assistant target to install for. + #[arg(long, value_enum, default_value = "both")] + target: SkillInstallTarget, + + /// Override the user home directory used for installation. + #[arg(long)] + home: Option, + + /// Print planned writes without changing the filesystem. + #[arg(long)] + dry_run: bool, + + /// Replace existing installed skill/plugin directories. + #[arg(long)] + force: bool, + + /// Emit machine-readable JSON. + #[arg(long)] + json: bool, + }, +} + +/// Supported skill installation targets. +#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)] +pub enum SkillInstallTarget { + /// Install the Codex skill under ~/.codex/skills. + Codex, + /// Install Claude Code skill and local plugin-dir package under ~/.claude. + Claude, + /// Install both Codex and Claude Code targets. + Both, } /// Main configuration structure diff --git a/src/main.rs b/src/main.rs index 6d8c75f..83b6390 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,9 +12,9 @@ use tracing::{debug, error, info}; use tracing_subscriber::{fmt, EnvFilter}; use embedded_debugger_mcp::{config::Args, tools::EmbeddedDebuggerToolHandler, Config}; +use skill_installer::{install_skill_bundle, DEFAULT_SKILL_PROMPT}; -const DEFAULT_SKILL_PROMPT: &str = - include_str!("../skills/embedded-debugger/references/default-prompt.md"); +mod skill_installer; #[tokio::main] async fn main() -> Result<(), Box> { @@ -133,12 +133,27 @@ async fn run_cli_command( } Ok(()) } - CliCommand::Skill { - action: SkillCommand::PrintPrompt, - } => { - print!("{}", DEFAULT_SKILL_PROMPT); - Ok(()) - } + CliCommand::Skill { action } => match action { + SkillCommand::PrintPrompt => { + print!("{}", DEFAULT_SKILL_PROMPT); + Ok(()) + } + SkillCommand::Install { + target, + home, + dry_run, + force, + json, + } => { + let report = install_skill_bundle(target, home, dry_run, force)?; + if json { + println!("{}", serde_json::to_string_pretty(&report)?); + } else { + report.print_text(); + } + Ok(()) + } + }, } } @@ -232,6 +247,7 @@ fn init_logging(config: &Config) -> Result<(), Box> { #[cfg(test)] mod tests { use super::*; + use embedded_debugger_mcp::config::SkillInstallTarget; #[test] fn test_args_parsing() { @@ -259,6 +275,31 @@ mod tests { ); } + #[test] + fn test_skill_install_subcommand_parsing() { + let args = Args::parse_from([ + "embedded-debugger-mcp", + "skill", + "install", + "--target", + "codex", + "--dry-run", + "--json", + ]); + assert_eq!( + args.command, + Some(CliCommand::Skill { + action: SkillCommand::Install { + target: SkillInstallTarget::Codex, + home: None, + dry_run: true, + force: false, + json: true, + } + }) + ); + } + #[test] fn test_default_config() { let config = Config::default(); diff --git a/src/skill_installer.rs b/src/skill_installer.rs new file mode 100644 index 0000000..95a778f --- /dev/null +++ b/src/skill_installer.rs @@ -0,0 +1,287 @@ +use embedded_debugger_mcp::config::SkillInstallTarget; +use serde::Serialize; +use std::path::{Path, PathBuf}; + +pub(crate) const DEFAULT_SKILL_PROMPT: &str = + include_str!("../skills/embedded-debugger/references/default-prompt.md"); + +const EMBEDDED_DEBUGGER_SKILL_MD: &str = include_str!("../skills/embedded-debugger/SKILL.md"); +const EMBEDDED_DEBUGGER_OPENAI_YAML: &str = + include_str!("../skills/embedded-debugger/agents/openai.yaml"); +const EMBEDDED_DEBUGGER_CLAUDE_PLUGIN_JSON: &str = include_str!("../.claude-plugin/plugin.json"); +const SKILL_NAME: &str = "embedded-debugger"; +const CLAUDE_PLUGIN_DIR_NAME: &str = "embedded-debugger-mcp"; + +#[derive(Debug, Serialize)] +pub(crate) struct SkillInstallReport { + target: String, + home: PathBuf, + dry_run: bool, + force: bool, + entries: Vec, + next_steps: Vec, +} + +impl SkillInstallReport { + pub(crate) fn print_text(&self) { + println!("embedded-debugger skill install"); + println!("target: {}", self.target); + println!("home: {}", self.home.display()); + println!("dry_run: {}", self.dry_run); + for entry in &self.entries { + println!( + "{}: {} ({})", + entry.kind, + entry.path.display(), + entry.status + ); + } + if !self.next_steps.is_empty() { + println!("next:"); + for step in &self.next_steps { + println!("- {}", step); + } + } + } +} + +#[derive(Debug, Serialize)] +struct SkillInstallEntry { + kind: &'static str, + path: PathBuf, + status: &'static str, +} + +#[derive(Debug, Clone, Copy)] +enum SkillInstallKind { + CodexSkill, + ClaudeSkill, + ClaudePlugin, +} + +impl SkillInstallKind { + fn label(self) -> &'static str { + match self { + Self::CodexSkill => "codex-skill", + Self::ClaudeSkill => "claude-skill", + Self::ClaudePlugin => "claude-plugin", + } + } + + fn path(self, home: &Path) -> PathBuf { + match self { + Self::CodexSkill => home.join(".codex").join("skills").join(SKILL_NAME), + Self::ClaudeSkill => home.join(".claude").join("skills").join(SKILL_NAME), + Self::ClaudePlugin => home + .join(".claude") + .join("plugins") + .join("local") + .join(CLAUDE_PLUGIN_DIR_NAME), + } + } + + fn write(self, path: &Path) -> std::io::Result<()> { + match self { + Self::CodexSkill | Self::ClaudeSkill => write_skill_directory(path), + Self::ClaudePlugin => write_claude_plugin_directory(path), + } + } +} + +pub(crate) fn install_skill_bundle( + target: SkillInstallTarget, + home: Option, + dry_run: bool, + force: bool, +) -> Result> { + let home = resolve_install_home(home)?; + let kinds = skill_install_kinds(target); + + if !dry_run && !force { + for kind in &kinds { + let path = kind.path(&home); + if path.exists() { + return Err(format!( + "{} already exists at {}; rerun with --force to replace it or --dry-run to preview", + kind.label(), + path.display() + ) + .into()); + } + } + } + + let mut entries = Vec::with_capacity(kinds.len()); + for kind in kinds { + let path = kind.path(&home); + let existed = path.exists(); + let status = if dry_run { + if existed { + "would_replace" + } else { + "would_install" + } + } else { + if existed { + remove_existing_path(&path)?; + } + kind.write(&path)?; + "installed" + }; + entries.push(SkillInstallEntry { + kind: kind.label(), + path, + status, + }); + } + + Ok(SkillInstallReport { + target: skill_install_target_label(target).to_string(), + home: home.clone(), + dry_run, + force, + next_steps: skill_install_next_steps(target, &home), + entries, + }) +} + +fn resolve_install_home(home: Option) -> Result> { + if let Some(home) = home { + return Ok(home); + } + std::env::var_os("HOME") + .or_else(|| std::env::var_os("USERPROFILE")) + .map(PathBuf::from) + .ok_or_else(|| "HOME is not set; pass --home ".into()) +} + +fn skill_install_kinds(target: SkillInstallTarget) -> Vec { + match target { + SkillInstallTarget::Codex => vec![SkillInstallKind::CodexSkill], + SkillInstallTarget::Claude => { + vec![ + SkillInstallKind::ClaudeSkill, + SkillInstallKind::ClaudePlugin, + ] + } + SkillInstallTarget::Both => vec![ + SkillInstallKind::CodexSkill, + SkillInstallKind::ClaudeSkill, + SkillInstallKind::ClaudePlugin, + ], + } +} + +fn skill_install_target_label(target: SkillInstallTarget) -> &'static str { + match target { + SkillInstallTarget::Codex => "codex", + SkillInstallTarget::Claude => "claude", + SkillInstallTarget::Both => "both", + } +} + +fn skill_install_next_steps(target: SkillInstallTarget, home: &Path) -> Vec { + let mut steps = Vec::new(); + if matches!(target, SkillInstallTarget::Codex | SkillInstallTarget::Both) { + steps.push("Use `$embedded-debugger` in Codex.".to_string()); + } + if matches!( + target, + SkillInstallTarget::Claude | SkillInstallTarget::Both + ) { + let plugin_path = SkillInstallKind::ClaudePlugin.path(home); + steps.push(format!( + "Use `claude --plugin-dir {} --print '/embedded-debugger inspect my embedded target setup'`.", + plugin_path.display() + )); + } + steps +} + +fn write_skill_directory(path: &Path) -> std::io::Result<()> { + write_text_file(&path.join("SKILL.md"), EMBEDDED_DEBUGGER_SKILL_MD)?; + write_text_file( + &path.join("agents").join("openai.yaml"), + EMBEDDED_DEBUGGER_OPENAI_YAML, + )?; + write_text_file( + &path.join("references").join("default-prompt.md"), + DEFAULT_SKILL_PROMPT, + )?; + Ok(()) +} + +fn write_claude_plugin_directory(path: &Path) -> std::io::Result<()> { + write_text_file( + &path.join(".claude-plugin").join("plugin.json"), + EMBEDDED_DEBUGGER_CLAUDE_PLUGIN_JSON, + )?; + write_skill_directory(&path.join("skills").join(SKILL_NAME)) +} + +fn write_text_file(path: &Path, content: &str) -> std::io::Result<()> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(path, content) +} + +fn remove_existing_path(path: &Path) -> std::io::Result<()> { + if path.is_dir() { + std::fs::remove_dir_all(path) + } else { + std::fs::remove_file(path) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_skill_install_writes_codex_and_claude_bundle() { + let home = tempfile::tempdir().expect("temp home"); + let report = install_skill_bundle( + SkillInstallTarget::Both, + Some(home.path().to_path_buf()), + false, + false, + ) + .expect("install skill bundle"); + + assert_eq!(report.entries.len(), 3); + assert!(home + .path() + .join(".codex/skills/embedded-debugger/SKILL.md") + .is_file()); + assert!(home + .path() + .join(".claude/skills/embedded-debugger/SKILL.md") + .is_file()); + assert!(home + .path() + .join(".claude/plugins/local/embedded-debugger-mcp/.claude-plugin/plugin.json") + .is_file()); + assert!(home + .path() + .join(".claude/plugins/local/embedded-debugger-mcp/skills/embedded-debugger/references/default-prompt.md") + .is_file()); + + let duplicate = install_skill_bundle( + SkillInstallTarget::Codex, + Some(home.path().to_path_buf()), + false, + false, + ); + assert!(duplicate.is_err()); + + let dry_run = install_skill_bundle( + SkillInstallTarget::Codex, + Some(home.path().to_path_buf()), + true, + false, + ) + .expect("dry-run existing install"); + assert_eq!(dry_run.entries[0].status, "would_replace"); + } +} From 8ef6336fdbb8a2f3ae451219deea41876180152e Mon Sep 17 00:00:00 2001 From: Hongda Chen <54146249+Adancurusul@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:43:34 +0800 Subject: [PATCH 7/8] ci: install libudev dependency --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index faa6617..f4f24bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,11 @@ jobs: - name: Install stable Rust uses: dtolnay/rust-toolchain@stable + - name: Install Linux system dependencies + run: | + sudo apt-get update + sudo apt-get install -y pkg-config libudev-dev + - name: Cache cargo uses: Swatinem/rust-cache@v2 From 9ea01771139e4e3f39677c51bd6120bb4dfec7fb Mon Sep 17 00:00:00 2001 From: Hongda Chen <54146249+Adancurusul@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:58:21 +0800 Subject: [PATCH 8/8] fix: enforce flash verify and doctor guards --- src/config.rs | 13 ++++-- src/main.rs | 68 +++++++++++++++++++----------- src/tools/debugger_tools/flash.rs | 6 +++ src/tools/debugger_tools/guards.rs | 29 ++++++++++++- tests/integration_tests.rs | 41 ++++++++++++++++++ 5 files changed, 127 insertions(+), 30 deletions(-) diff --git a/src/config.rs b/src/config.rs index 5db92cb..940117d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -198,14 +198,19 @@ impl Default for Config { impl Config { /// Load configuration from file or create default pub fn load(config_path: Option<&PathBuf>) -> Result { + let config = Self::load_unvalidated(config_path)?; + config.validate()?; + Ok(config) + } + + /// Load configuration from file or create default without validating values. + pub fn load_unvalidated(config_path: Option<&PathBuf>) -> Result { if let Some(path) = config_path { let content = std::fs::read_to_string(path).map_err(|e| { DebugError::InvalidConfig(format!("Failed to read config file: {}", e)) })?; - let config: Config = toml::from_str(&content) - .map_err(|e| DebugError::InvalidConfig(format!("Invalid TOML syntax: {}", e)))?; - config.validate()?; - Ok(config) + toml::from_str(&content) + .map_err(|e| DebugError::InvalidConfig(format!("Invalid TOML syntax: {}", e))) } else { Ok(Config::default()) } diff --git a/src/main.rs b/src/main.rs index 83b6390..5f84178 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use std::process::Command as ProcessCommand; use tracing::{debug, error, info}; use tracing_subscriber::{fmt, EnvFilter}; -use embedded_debugger_mcp::{config::Args, tools::EmbeddedDebuggerToolHandler, Config}; +use embedded_debugger_mcp::{config::Args, tools::EmbeddedDebuggerToolHandler, Config, DebugError}; use skill_installer::{install_skill_bundle, DEFAULT_SKILL_PROMPT}; mod skill_installer; @@ -35,6 +35,16 @@ async fn main() -> Result<(), Box> { return Ok(()); } + if let Some(CliCommand::Doctor { json }) = args.command.clone() { + let mut config_result = Config::load_unvalidated(args.config.as_ref()); + if let Ok(config) = &mut config_result { + config.merge_args(&args); + } + let report = DoctorReport::collect(config_result); + print_doctor_report(&report, json)?; + return Ok(()); + } + // Load configuration let mut config = Config::load(args.config.as_ref())?; @@ -110,28 +120,8 @@ async fn run_cli_command( Ok(()) } CliCommand::Doctor { json } => { - let report = DoctorReport::collect(&config); - if json { - println!("{}", serde_json::to_string_pretty(&report)?); - } else { - println!("embedded-debugger-mcp doctor"); - println!("version: {}", report.version); - println!( - "rustc: {}", - report.rustc_version.as_deref().unwrap_or("not found") - ); - println!("config_valid: {}", report.config_valid); - if let Some(error) = &report.config_error { - println!("config_error: {}", error); - } - println!("probe_count: {}", report.probe_count.unwrap_or(0)); - if let Some(error) = &report.probe_error { - println!("probe_error: {}", error); - } - println!("mcp_mode: use `embedded-debugger-mcp serve`"); - println!("cli_skill_mode: use `embedded-debugger-mcp skill print-prompt`"); - } - Ok(()) + let report = DoctorReport::collect(Ok(config)); + print_doctor_report(&report, json) } CliCommand::Skill { action } => match action { SkillCommand::PrintPrompt => { @@ -189,8 +179,8 @@ struct DoctorReport { } impl DoctorReport { - fn collect(config: &Config) -> Self { - let config_result = config.validate(); + fn collect(config_result: Result) -> Self { + let config_result = config_result.and_then(|config| config.validate().map(|()| config)); let probe_result = ProbeDiscovery::list_probes(); Self { @@ -204,6 +194,34 @@ impl DoctorReport { } } +fn print_doctor_report( + report: &DoctorReport, + json: bool, +) -> Result<(), Box> { + if json { + println!("{}", serde_json::to_string_pretty(report)?); + return Ok(()); + } + + println!("embedded-debugger-mcp doctor"); + println!("version: {}", report.version); + println!( + "rustc: {}", + report.rustc_version.as_deref().unwrap_or("not found") + ); + println!("config_valid: {}", report.config_valid); + if let Some(error) = &report.config_error { + println!("config_error: {}", error); + } + println!("probe_count: {}", report.probe_count.unwrap_or(0)); + if let Some(error) = &report.probe_error { + println!("probe_error: {}", error); + } + println!("mcp_mode: use `embedded-debugger-mcp serve`"); + println!("cli_skill_mode: use `embedded-debugger-mcp skill print-prompt`"); + Ok(()) +} + fn command_output(command: &str, args: &[&str]) -> Option { let output = ProcessCommand::new(command).args(args).output().ok()?; if !output.status.success() { diff --git a/src/tools/debugger_tools/flash.rs b/src/tools/debugger_tools/flash.rs index 80cbc64..3155224 100644 --- a/src/tools/debugger_tools/flash.rs +++ b/src/tools/debugger_tools/flash.rs @@ -283,6 +283,12 @@ impl EmbeddedDebuggerToolHandler { &expected_data }; + self.ensure_memory_read_allowed_for_target( + &session_arc.target_chip, + address, + expected_data.len(), + )?; + // Perform verification { let mut session = session_arc.session.lock().await; diff --git a/src/tools/debugger_tools/guards.rs b/src/tools/debugger_tools/guards.rs index ee8caf4..e4b007a 100644 --- a/src/tools/debugger_tools/guards.rs +++ b/src/tools/debugger_tools/guards.rs @@ -30,6 +30,15 @@ impl EmbeddedDebuggerToolHandler { session: &DebugSession, address: u64, size: usize, + ) -> Result<(), McpError> { + self.ensure_memory_read_allowed_for_target(&session.target_chip, address, size) + } + + pub(super) fn ensure_memory_read_allowed_for_target( + &self, + target_chip: &str, + address: u64, + size: usize, ) -> Result<(), McpError> { if size == 0 { return Err(McpError::internal_error( @@ -46,7 +55,7 @@ impl EmbeddedDebuggerToolHandler { None, )); } - self.ensure_memory_region_allowed(session, address, size, 'r') + self.ensure_memory_region_allowed_for_target(target_chip, address, size, 'r') } pub(super) fn ensure_memory_write_allowed( @@ -388,4 +397,22 @@ mod tests { ) .is_err()); } + + #[test] + fn restricted_flash_verify_reads_use_memory_policy() { + let mut config = Config::default(); + config.security.restrict_memory_access = true; + config.memory.max_read_size = 16; + let handler = EmbeddedDebuggerToolHandler::new(config); + + assert!(handler + .ensure_memory_read_allowed_for_target("STM32F407VGTx", 0x0800_0000, 16) + .is_ok()); + assert!(handler + .ensure_memory_read_allowed_for_target("STM32F407VGTx", 0x0800_0000, 17) + .is_err()); + assert!(handler + .ensure_memory_read_allowed_for_target("STM32F407VGTx", 0x4000_0000, 4) + .is_err()); + } } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 2c7b28f..13aa9e1 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -3,6 +3,7 @@ use clap::Parser; use embedded_debugger_mcp::config::Args; use embedded_debugger_mcp::Config; +use std::process::Command; #[tokio::test] async fn test_config_validation() { @@ -67,6 +68,46 @@ fn test_cli_explicit_values_override_config_file_values() { assert!(config.security.restrict_memory_access); } +#[test] +fn test_load_unvalidated_keeps_doctor_reportable_config() { + let mut config = Config::default(); + config.server.max_sessions = 0; + let temp = tempfile::NamedTempFile::new().unwrap(); + std::fs::write(temp.path(), config.to_toml().unwrap()).unwrap(); + let path = temp.path().to_path_buf(); + + let unvalidated = Config::load_unvalidated(Some(&path)).unwrap(); + assert!(unvalidated.validate().is_err()); + assert!(Config::load(Some(&path)).is_err()); +} + +#[test] +fn test_doctor_json_reports_bad_config() { + let temp = tempfile::NamedTempFile::new().unwrap(); + std::fs::write(temp.path(), "not = [").unwrap(); + + let output = Command::new(env!("CARGO_BIN_EXE_embedded-debugger-mcp")) + .arg("--config") + .arg(temp.path()) + .arg("doctor") + .arg("--json") + .output() + .unwrap(); + + assert!( + output.status.success(), + "doctor should report config errors as JSON: stderr={}", + String::from_utf8_lossy(&output.stderr) + ); + + let report: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(report["config_valid"], false); + assert!(report["config_error"] + .as_str() + .unwrap() + .contains("Invalid TOML syntax")); +} + #[test] fn test_error_types() { use embedded_debugger_mcp::DebugError;