diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 9e518063..2c8d1a99 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,13 +1,8 @@ { "servers": { - "aimdb": { - "type": "stdio", - "command": "/aimdb_ws/aimdb/target/release/aimdb-mcp", - "args": [], - "env": { - "RUST_LOG": "info", - "AIMDB_WORKSPACE": "${workspaceFolder}" - } + "aimdb-weather": { + "type": "http", + "url": "http://aimdb.dev/mcp" } } } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f9f1da1..2d2e40a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **MCP public mode**: New `--public` flag restricts the MCP server to read-only tools for safe internet-facing deployments with SSRF protection ([tools/aimdb-mcp](tools/aimdb-mcp/CHANGELOG.md)) +- **MCP `--socket` flag**: Default socket path can be set at startup, simplifying single-instance workflows ([tools/aimdb-mcp](tools/aimdb-mcp/CHANGELOG.md)) - **Context-Aware Deserializers (Design 026)**: Inbound connector deserializers can now receive a `RuntimeContext` for platform-independent timestamps and logging - New `.with_deserializer(|ctx, bytes| ...)` API on `InboundConnectorBuilder` provides `RuntimeContext` to deserialization closures - New `.with_deserializer_raw(|bytes| ...)` for plain bytes-only deserialization when context is unnecessary @@ -50,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **aimdb-knx-connector**: Updated router dispatch for new `route()` signature; outbound publishers dispatch via `SerializerKind` - **aimdb-websocket-connector**: Updated router dispatch for new `route()` signature; outbound publishers dispatch via `SerializerKind` - All connector examples updated to use new `.with_deserializer(|_ctx, bytes| ...)` and `.with_serializer_raw(|value| ...)` signatures +- **`rand` 0.8 → 0.10.1**: Upgraded across workspace (`aimdb-data-contracts`, `aimdb-embassy-adapter`, examples). Migrated API: `gen` → `random`, `SmallRng` seed size 16 → 32, added `RngExt` imports. +- **README**: Reordered quickstart — remote MCP exploration (zero install) is now step 2, local Docker setup moved to step 3. ## [1.0.0] - 2026-03-16 diff --git a/Cargo.lock b/Cargo.lock index e8ab89a8..3d4a2a3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,7 +99,7 @@ version = "0.1.0" dependencies = [ "aimdb-core", "aimdb-executor", - "rand 0.8.5", + "rand 0.10.1", "serde", "serde_json", ] @@ -130,7 +130,7 @@ dependencies = [ "futures", "futures-core", "heapless 0.9.1", - "rand 0.8.5", + "rand 0.10.1", "tracing", "tracing-test", ] @@ -180,6 +180,7 @@ dependencies = [ "aimdb-core", "anyhow", "chrono", + "clap", "fs2", "once_cell", "serde", @@ -622,6 +623,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + [[package]] name = "chrono" version = "0.4.42" @@ -783,6 +795,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -1043,7 +1064,7 @@ dependencies = [ "knx-connector-demo-common", "micromath", "panic-probe", - "rand 0.8.5", + "rand 0.10.1", "static_cell", "stm32-fmc 0.3.2", ] @@ -1076,7 +1097,7 @@ dependencies = [ "micromath", "mqtt-connector-demo-common", "panic-probe", - "rand 0.8.5", + "rand 0.10.1", "static_cell", "stm32-fmc 0.3.2", ] @@ -1594,8 +1615,22 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.0", "wasip2", + "wasip3", ] [[package]] @@ -1931,6 +1966,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1966,6 +2007,8 @@ checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", + "serde", + "serde_core", ] [[package]] @@ -2031,6 +2074,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.177" @@ -2465,15 +2514,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand" -version = "0.8.5" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" @@ -2481,18 +2525,19 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.3", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "rand" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", ] [[package]] @@ -2510,9 +2555,6 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] [[package]] name = "rand_core" @@ -2687,7 +2729,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver", + "semver 0.9.0", ] [[package]] @@ -2873,6 +2915,12 @@ dependencies = [ "semver-parser", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "semver-parser" version = "0.7.0" @@ -2985,7 +3033,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -3110,7 +3158,7 @@ dependencies = [ [[package]] name = "stm32-metapac" version = "21.0.0" -source = "git+https://github.com/embassy-rs/stm32-data-generated?tag=stm32-data-68326d96233978fbbfdc55c29cca8624dab43cd6#714b6bd91a4ca13c5b5b6a14c68a42de790e8b55" +source = "git+https://github.com/embassy-rs/stm32-data-generated?tag=stm32-data-5f15bbfaf37bd6a6330fec66a3a39de0042d64ac#8da545d80e11594b00efdcc6fb94c70054bee09a" dependencies = [ "cortex-m", "cortex-m-rt", @@ -3706,6 +3754,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -3829,7 +3883,16 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] @@ -3914,6 +3977,40 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver 1.0.28", +] + [[package]] name = "weather-hub" version = "0.1.0" @@ -3934,7 +4031,7 @@ version = "0.1.0" dependencies = [ "aimdb-core", "aimdb-data-contracts", - "rand 0.8.5", + "rand 0.10.1", "serde", "serde_json", ] @@ -3966,7 +4063,7 @@ dependencies = [ "aimdb-mqtt-connector", "aimdb-tokio-adapter", "chrono", - "rand 0.8.5", + "rand 0.10.1", "tokio", "tracing", "tracing-subscriber", @@ -4001,7 +4098,7 @@ dependencies = [ "heapless 0.8.0", "micromath", "panic-probe", - "rand 0.8.5", + "rand 0.10.1", "static_cell", "stm32-fmc 0.3.2", "weather-mesh-common", @@ -4412,6 +4509,94 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.108", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.108", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver 1.0.28", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.2" diff --git a/README.md b/README.md index 4b346a6d..c3301501 100644 --- a/README.md +++ b/README.md @@ -180,45 +180,41 @@ Explore a running sensor mesh — no setup required: > **[aimdb.dev](https://aimdb.dev)** — live weather stations streaming typed contracts across MCU, edge and cloud. -#### 2. Run locally +#### 2. Explore with AI -Spin up a full MCU → edge → cloud mesh with one command: - -```bash -cd examples/weather-mesh-demo -docker compose up -``` - -This starts three weather stations, an MQTT broker and a central hub — all wired together with typed data contracts. - -#### 3. Explore with AI - -With the mesh running, connect an MCP-compatible editor to query your data in natural language: +Connect an MCP-compatible editor to the live demo and query your data in natural language — no install required:

AimDB MCP Live Demo

-Install the MCP server and add it to your workspace: - -```bash -cargo install aimdb-mcp -``` +Add the remote MCP server to your workspace: `.vscode/mcp.json`: ```json { "servers": { - "aimdb": { - "type": "stdio", - "command": "${userHome}/.cargo/bin/aimdb-mcp" + "aimdb-weather": { + "type": "http", + "url": "http://aimdb.dev/mcp" } } } ``` -Then ask: *"What's the current temperature from station alpha?"* — see the [MCP server docs](tools/aimdb-mcp/) for Claude Desktop and other editors. +Then ask: *"What's the current temperature in Munich?"* — see the [MCP server docs](tools/aimdb-mcp/) for Claude Desktop and other editors. + +#### 3. Run locally + +Spin up a full MCU → edge → cloud mesh with one command: + +```bash +cd examples/weather-mesh-demo +docker compose up +``` + +This starts three weather stations, an MQTT broker and a central hub — all wired together with typed data contracts. #### 4. Build your own diff --git a/_external/embassy b/_external/embassy index 1781e4a4..a1b22839 160000 --- a/_external/embassy +++ b/_external/embassy @@ -1 +1 @@ -Subproject commit 1781e4a4d573cc6439763c5f16a64458d100d955 +Subproject commit a1b22839eb832befb20db9d0b8f41becb91541a8 diff --git a/aimdb-data-contracts/CHANGELOG.md b/aimdb-data-contracts/CHANGELOG.md index 86bc8b3e..823a48de 100644 --- a/aimdb-data-contracts/CHANGELOG.md +++ b/aimdb-data-contracts/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -No changes yet. +### Changed + +- **Dependency update**: Upgraded `rand` from 0.8 to 0.10.1. ## [0.1.0] - 2026-03-16 diff --git a/aimdb-data-contracts/Cargo.toml b/aimdb-data-contracts/Cargo.toml index 2e845cd3..23650238 100644 --- a/aimdb-data-contracts/Cargo.toml +++ b/aimdb-data-contracts/Cargo.toml @@ -31,7 +31,7 @@ aimdb-core = { version = "1.0.0", path = "../aimdb-core", optional = true, defau aimdb-executor = { version = "0.1.0", path = "../aimdb-executor", optional = true, default-features = false } [dependencies.rand] -version = "0.8" +version = "0.10.1" optional = true default-features = false features = ["std_rng"] diff --git a/aimdb-embassy-adapter/CHANGELOG.md b/aimdb-embassy-adapter/CHANGELOG.md index c5156881..d2a1fed6 100644 --- a/aimdb-embassy-adapter/CHANGELOG.md +++ b/aimdb-embassy-adapter/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -No changes yet. +### Changed + +- **Dev-dependency update**: Upgraded `rand` from 0.8 to 0.10.1. ## [0.5.0] - 2026-02-21 diff --git a/aimdb-embassy-adapter/Cargo.toml b/aimdb-embassy-adapter/Cargo.toml index 96b7a787..1b30b11a 100644 --- a/aimdb-embassy-adapter/Cargo.toml +++ b/aimdb-embassy-adapter/Cargo.toml @@ -80,4 +80,4 @@ futures = "0.3" tracing-test = "0.2" # Random number generation for tests -rand = "0.8" +rand = "0.10.1" diff --git a/examples/embassy-knx-connector-demo/Cargo.toml b/examples/embassy-knx-connector-demo/Cargo.toml index dd6b4cde..b155e6af 100644 --- a/examples/embassy-knx-connector-demo/Cargo.toml +++ b/examples/embassy-knx-connector-demo/Cargo.toml @@ -84,7 +84,7 @@ stm32-fmc = { workspace = true } embedded-alloc = { version = "0.6", features = ["llff"] } # RNG for unique client IDs -rand = { version = "0.8", default-features = false, features = ["small_rng"] } +rand = { version = "0.10.1", default-features = false } [package.metadata.embassy] build = [ diff --git a/examples/embassy-knx-connector-demo/src/main.rs b/examples/embassy-knx-connector-demo/src/main.rs index 7badbfb3..1bdf9793 100644 --- a/examples/embassy-knx-connector-demo/src/main.rs +++ b/examples/embassy-knx-connector-demo/src/main.rs @@ -171,18 +171,18 @@ async fn main(spawner: Spawner) { mode: HseMode::BypassDigital, }); config.rcc.pll1 = Some(Pll { - source: PllSource::HSE, - prediv: PllPreDiv::DIV2, - mul: PllMul::MUL125, - divp: Some(PllDiv::DIV2), - divq: Some(PllDiv::DIV2), + source: PllSource::Hse, + prediv: PllPreDiv::Div2, + mul: PllMul::Mul125, + divp: Some(PllDiv::Div2), + divq: Some(PllDiv::Div2), divr: None, }); - config.rcc.ahb_pre = AHBPrescaler::DIV1; - config.rcc.apb1_pre = APBPrescaler::DIV1; - config.rcc.apb2_pre = APBPrescaler::DIV1; - config.rcc.apb3_pre = APBPrescaler::DIV1; - config.rcc.sys = Sysclk::PLL1_P; + config.rcc.ahb_pre = AHBPrescaler::Div1; + config.rcc.apb1_pre = APBPrescaler::Div1; + config.rcc.apb2_pre = APBPrescaler::Div1; + config.rcc.apb3_pre = APBPrescaler::Div1; + config.rcc.sys = Sysclk::Pll1P; config.rcc.voltage_scale = VoltageScale::Scale0; } let p = embassy_stm32::init(config); diff --git a/examples/embassy-mqtt-connector-demo/Cargo.toml b/examples/embassy-mqtt-connector-demo/Cargo.toml index 9662791a..9e46f7c1 100644 --- a/examples/embassy-mqtt-connector-demo/Cargo.toml +++ b/examples/embassy-mqtt-connector-demo/Cargo.toml @@ -85,7 +85,7 @@ stm32-fmc = { workspace = true } embedded-alloc = { version = "0.6", features = ["llff"] } # RNG for unique client IDs -rand = { version = "0.8", default-features = false, features = ["small_rng"] } +rand = { version = "0.10.1", default-features = false } [package.metadata.embassy] build = [ diff --git a/examples/embassy-mqtt-connector-demo/src/main.rs b/examples/embassy-mqtt-connector-demo/src/main.rs index cd4d23f0..894f0ef7 100644 --- a/examples/embassy-mqtt-connector-demo/src/main.rs +++ b/examples/embassy-mqtt-connector-demo/src/main.rs @@ -220,18 +220,18 @@ async fn main(spawner: Spawner) { mode: HseMode::BypassDigital, }); config.rcc.pll1 = Some(Pll { - source: PllSource::HSE, - prediv: PllPreDiv::DIV2, - mul: PllMul::MUL125, - divp: Some(PllDiv::DIV2), - divq: Some(PllDiv::DIV2), + source: PllSource::Hse, + prediv: PllPreDiv::Div2, + mul: PllMul::Mul125, + divp: Some(PllDiv::Div2), + divq: Some(PllDiv::Div2), divr: None, }); - config.rcc.ahb_pre = AHBPrescaler::DIV1; - config.rcc.apb1_pre = APBPrescaler::DIV1; - config.rcc.apb2_pre = APBPrescaler::DIV1; - config.rcc.apb3_pre = APBPrescaler::DIV1; - config.rcc.sys = Sysclk::PLL1_P; + config.rcc.ahb_pre = AHBPrescaler::Div1; + config.rcc.apb1_pre = APBPrescaler::Div1; + config.rcc.apb2_pre = APBPrescaler::Div1; + config.rcc.apb3_pre = APBPrescaler::Div1; + config.rcc.sys = Sysclk::Pll1P; config.rcc.voltage_scale = VoltageScale::Scale0; } let p = embassy_stm32::init(config); diff --git a/examples/weather-mesh-demo/weather-mesh-common/Cargo.toml b/examples/weather-mesh-demo/weather-mesh-common/Cargo.toml index 61da1c4b..186e93ef 100644 --- a/examples/weather-mesh-demo/weather-mesh-common/Cargo.toml +++ b/examples/weather-mesh-demo/weather-mesh-common/Cargo.toml @@ -21,6 +21,6 @@ aimdb-core = { path = "../../../aimdb-core", default-features = false, features aimdb-data-contracts = { path = "../../../aimdb-data-contracts", default-features = false } serde = { version = "1.0", default-features = false, features = ["derive"] } serde_json = { version = "1.0", optional = true } -rand = { version = "0.8", optional = true, default-features = false, features = [ +rand = { version = "0.10.1", optional = true, default-features = false, features = [ "std_rng", ] } diff --git a/examples/weather-mesh-demo/weather-mesh-common/src/contracts/humidity.rs b/examples/weather-mesh-demo/weather-mesh-common/src/contracts/humidity.rs index d7b5784e..c0dce7ea 100644 --- a/examples/weather-mesh-demo/weather-mesh-common/src/contracts/humidity.rs +++ b/examples/weather-mesh-demo/weather-mesh-common/src/contracts/humidity.rs @@ -10,6 +10,8 @@ use aimdb_data_contracts::Linkable; #[cfg(feature = "simulatable")] use aimdb_data_contracts::{Simulatable, SimulationConfig}; +#[cfg(feature = "simulatable")] +use rand::RngExt; /// Humidity sensor reading #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -70,12 +72,12 @@ impl Simulatable for Humidity { // Random walk: small delta from previous value, clamped to valid range let current = match previous { Some(prev) => { - let delta = (rng.gen::() - 0.5) * variation * step; + let delta = (rng.random::() - 0.5) * variation * step; (prev.percent + delta + trend) .clamp(0.0, 100.0) .clamp(base - variation, base + variation) } - None => base + (rng.gen::() - 0.5) * variation, + None => base + (rng.random::() - 0.5) * variation, }; Humidity { diff --git a/examples/weather-mesh-demo/weather-mesh-common/src/contracts/location.rs b/examples/weather-mesh-demo/weather-mesh-common/src/contracts/location.rs index ae54e364..a4f8bb70 100644 --- a/examples/weather-mesh-demo/weather-mesh-common/src/contracts/location.rs +++ b/examples/weather-mesh-demo/weather-mesh-common/src/contracts/location.rs @@ -10,6 +10,8 @@ use aimdb_data_contracts::Linkable; #[cfg(feature = "simulatable")] use aimdb_data_contracts::{Simulatable, SimulationConfig}; +#[cfg(feature = "simulatable")] +use rand::RngExt; /// GPS location reading #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -93,8 +95,8 @@ impl Simulatable for GpsLocation { // Random walk from previous position or start near base let (lat, lon) = match previous { Some(prev) => { - let lat_delta = (rng.gen::() - 0.5) * max_delta * step; - let lon_delta = (rng.gen::() - 0.5) * max_delta * step; + let lat_delta = (rng.random::() - 0.5) * max_delta * step; + let lon_delta = (rng.random::() - 0.5) * max_delta * step; let new_lat = (prev.latitude + lat_delta).clamp(base_lat - max_delta, base_lat + max_delta); let new_lon = @@ -102,8 +104,8 @@ impl Simulatable for GpsLocation { (new_lat, new_lon) } None => { - let lat = base_lat + (rng.gen::() - 0.5) * max_delta; - let lon = base_lon + (rng.gen::() - 0.5) * max_delta; + let lat = base_lat + (rng.random::() - 0.5) * max_delta; + let lon = base_lon + (rng.random::() - 0.5) * max_delta; (lat, lon) } }; @@ -111,8 +113,8 @@ impl Simulatable for GpsLocation { GpsLocation { latitude: lat, longitude: lon, - altitude: Some(200.0 + rng.gen::() * 10.0), - accuracy: Some(5.0 + rng.gen::() * 10.0), + altitude: Some(200.0 + rng.random::() * 10.0), + accuracy: Some(5.0 + rng.random::() * 10.0), timestamp, } } diff --git a/examples/weather-mesh-demo/weather-mesh-common/src/contracts/temperature.rs b/examples/weather-mesh-demo/weather-mesh-common/src/contracts/temperature.rs index c15a66b2..74b09d0a 100644 --- a/examples/weather-mesh-demo/weather-mesh-common/src/contracts/temperature.rs +++ b/examples/weather-mesh-demo/weather-mesh-common/src/contracts/temperature.rs @@ -21,6 +21,8 @@ use aimdb_data_contracts::Linkable; #[cfg(feature = "simulatable")] use aimdb_data_contracts::{Simulatable, SimulationConfig}; +#[cfg(feature = "simulatable")] +use rand::RngExt; #[cfg(feature = "migratable")] use aimdb_data_contracts::{MigrationError, MigrationStep}; @@ -208,10 +210,10 @@ impl Simulatable for TemperatureV2 { // Random walk: small delta from previous value, clamped to range let current = match previous { Some(prev) => { - let delta = (rng.gen::() - 0.5) * variation * step; + let delta = (rng.random::() - 0.5) * variation * step; (prev.celsius + delta + trend).clamp(base - variation, base + variation) } - None => base + (rng.gen::() - 0.5) * variation, + None => base + (rng.random::() - 0.5) * variation, }; TemperatureV2 { diff --git a/examples/weather-mesh-demo/weather-station-beta/Cargo.toml b/examples/weather-mesh-demo/weather-station-beta/Cargo.toml index ec8be7ad..c88c7673 100644 --- a/examples/weather-mesh-demo/weather-station-beta/Cargo.toml +++ b/examples/weather-mesh-demo/weather-station-beta/Cargo.toml @@ -30,5 +30,5 @@ aimdb-mqtt-connector = { path = "../../../aimdb-mqtt-connector", features = [ tokio = { version = "1", features = ["full"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -rand = "0.8" +rand = "0.10.1" chrono = "0.4" diff --git a/examples/weather-mesh-demo/weather-station-beta/src/main.rs b/examples/weather-mesh-demo/weather-station-beta/src/main.rs index 3fc307bd..13a44c0c 100644 --- a/examples/weather-mesh-demo/weather-station-beta/src/main.rs +++ b/examples/weather-mesh-demo/weather-station-beta/src/main.rs @@ -110,7 +110,7 @@ async fn main() -> Result<(), Box> { }; // Create RNG (StdRng is Send, ThreadRng is not) - let mut rng = rand::rngs::StdRng::from_entropy(); + let mut rng = rand::rngs::StdRng::from_rng(&mut rand::rng()); let mut prev_temp: Option = None; let mut prev_humidity: Option = None; diff --git a/examples/weather-mesh-demo/weather-station-gamma/Cargo.toml b/examples/weather-mesh-demo/weather-station-gamma/Cargo.toml index 4fb713e8..55256d93 100644 --- a/examples/weather-mesh-demo/weather-station-gamma/Cargo.toml +++ b/examples/weather-mesh-demo/weather-station-gamma/Cargo.toml @@ -88,7 +88,7 @@ stm32-fmc = { workspace = true } embedded-alloc = { version = "0.6", features = ["llff"] } # Random number generation for simulation -rand = { version = "0.8", default-features = false, features = ["small_rng"] } +rand = { version = "0.10.1", default-features = false } [profile.release] opt-level = "s" diff --git a/examples/weather-mesh-demo/weather-station-gamma/src/main.rs b/examples/weather-mesh-demo/weather-station-gamma/src/main.rs index 23e603c6..d5cb8286 100644 --- a/examples/weather-mesh-demo/weather-station-gamma/src/main.rs +++ b/examples/weather-mesh-demo/weather-station-gamma/src/main.rs @@ -85,7 +85,7 @@ async fn temperature_producer( }, }; - let mut rng = rand::rngs::SmallRng::from_seed([42; 16]); + let mut rng = rand::rngs::SmallRng::from_seed([42; 32]); let mut prev: Option = None; loop { @@ -122,7 +122,7 @@ async fn humidity_producer( }, }; - let mut rng = rand::rngs::SmallRng::from_seed([84; 16]); + let mut rng = rand::rngs::SmallRng::from_seed([84; 32]); let mut prev: Option = None; loop { @@ -168,18 +168,18 @@ async fn main(spawner: Spawner) { mode: HseMode::BypassDigital, }); config.rcc.pll1 = Some(Pll { - source: PllSource::HSE, - prediv: PllPreDiv::DIV2, - mul: PllMul::MUL125, - divp: Some(PllDiv::DIV2), - divq: Some(PllDiv::DIV2), + source: PllSource::Hse, + prediv: PllPreDiv::Div2, + mul: PllMul::Mul125, + divp: Some(PllDiv::Div2), + divq: Some(PllDiv::Div2), divr: None, }); - config.rcc.ahb_pre = AHBPrescaler::DIV1; - config.rcc.apb1_pre = APBPrescaler::DIV1; - config.rcc.apb2_pre = APBPrescaler::DIV1; - config.rcc.apb3_pre = APBPrescaler::DIV1; - config.rcc.sys = Sysclk::PLL1_P; + config.rcc.ahb_pre = AHBPrescaler::Div1; + config.rcc.apb1_pre = APBPrescaler::Div1; + config.rcc.apb2_pre = APBPrescaler::Div1; + config.rcc.apb3_pre = APBPrescaler::Div1; + config.rcc.sys = Sysclk::Pll1P; config.rcc.voltage_scale = VoltageScale::Scale0; } let p = embassy_stm32::init(config); diff --git a/tools/aimdb-mcp/CHANGELOG.md b/tools/aimdb-mcp/CHANGELOG.md index 922c7122..8e99333e 100644 --- a/tools/aimdb-mcp/CHANGELOG.md +++ b/tools/aimdb-mcp/CHANGELOG.md @@ -7,7 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -No changes yet. +### Added + +- **Public mode** (`--public` flag): Restricts the server to read-only tools (`discover_instances`, `list_records`, `get_record`) for safe internet-facing deployments. Suppresses resources and prompts capabilities. Strips client-supplied `socket_path` arguments to prevent SSRF. +- **Default socket flag** (`--socket `): Sets a default socket path at startup, removing the need for clients to pass `socket_path` on every call. Resolution order: explicit arg → `--socket` → `AIMDB_SOCKET` env var. +- **CLI argument parsing**: Added `clap` dependency for structured CLI flags. +- **Comprehensive tests**: Public mode tool filtering, tool rejection, socket stripping, and capability suppression. ## [0.7.0] - 2026-03-24 diff --git a/tools/aimdb-mcp/Cargo.toml b/tools/aimdb-mcp/Cargo.toml index 3e1786c7..9c2ef6ed 100644 --- a/tools/aimdb-mcp/Cargo.toml +++ b/tools/aimdb-mcp/Cargo.toml @@ -46,6 +46,9 @@ tokio = { version = "1.48", features = [ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +# CLI +clap = { version = "4", features = ["derive"] } + # Error handling anyhow = "1.0" thiserror = "1.0" diff --git a/tools/aimdb-mcp/src/main.rs b/tools/aimdb-mcp/src/main.rs index 1c0e78d9..abee943c 100644 --- a/tools/aimdb-mcp/src/main.rs +++ b/tools/aimdb-mcp/src/main.rs @@ -9,11 +9,34 @@ use aimdb_mcp::protocol::{ JsonRpcResponse, PromptsGetParams, ResourceReadParams, ToolCallParams, }; use aimdb_mcp::{McpServer, StdioTransport}; +use clap::Parser; use serde_json::Value; use tracing::{debug, error, info, warn}; +/// AimDB MCP Server — LLM-powered introspection for AimDB instances. +#[derive(Parser)] +#[command(version, about)] +struct Cli { + /// Public mode: expose only read-only tools (discover_instances, list_records, get_record). + /// Intended for untrusted / internet-facing endpoints. + #[arg(long)] + public: bool, + + /// Default Unix socket path for AimDB connections. + /// Tools will use this instead of requiring an explicit socket_path argument. + /// Equivalent to setting the AIMDB_SOCKET environment variable. + #[arg(long, value_name = "PATH")] + socket: Option, +} + #[tokio::main] async fn main() { + let cli = Cli::parse(); + + if let Some(ref socket) = cli.socket { + aimdb_mcp::tools::set_default_socket(socket.clone()); + } + // Initialize tracing tracing_subscriber::fmt() .with_env_filter( @@ -26,9 +49,15 @@ async fn main() { info!("🚀 Starting AimDB MCP Server"); info!("📡 Protocol: MCP 2025-06-18 over JSON-RPC 2.0"); info!("🔌 Transport: stdio (NDJSON)"); + if cli.public { + info!("🔒 Public mode: only read-only tools are available"); + } + if let Some(ref socket) = cli.socket { + info!("📎 Default socket: {}", socket); + } // Create server and transport - let server = McpServer::new(); + let server = McpServer::new().with_public_mode(cli.public); let mut transport = StdioTransport::new(); info!("✅ Server ready, waiting for initialize request..."); diff --git a/tools/aimdb-mcp/src/server.rs b/tools/aimdb-mcp/src/server.rs index d7ccc680..4d9b7483 100644 --- a/tools/aimdb-mcp/src/server.rs +++ b/tools/aimdb-mcp/src/server.rs @@ -17,6 +17,9 @@ use std::sync::Arc; use tokio::sync::Mutex; use tracing::debug; +/// Tools exposed in public mode (read-only, safe for untrusted clients). +const PUBLIC_TOOLS: &[&str] = &["discover_instances", "list_records", "get_record"]; + /// MCP server state #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ServerState { @@ -32,6 +35,8 @@ pub enum ServerState { pub struct McpServer { state: Arc>, connection_pool: ConnectionPool, + /// When true, only PUBLIC_TOOLS are advertised and callable. + public_mode: bool, } impl McpServer { @@ -40,9 +45,21 @@ impl McpServer { Self { state: Arc::new(Mutex::new(ServerState::Uninitialized)), connection_pool: ConnectionPool::new(), + public_mode: false, } } + /// Enable public mode: only read-only tools are available. + pub fn with_public_mode(mut self, enabled: bool) -> Self { + self.public_mode = enabled; + self + } + + /// Returns true if the server is in public (restricted) mode. + pub fn is_public(&self) -> bool { + self.public_mode + } + /// Get the connection pool pub fn connection_pool(&self) -> &ConnectionPool { &self.connection_pool @@ -83,17 +100,25 @@ impl McpServer { // Initialize session store for architecture agent crate::architecture::init_session_store(); - // Build server capabilities + // Build server capabilities — in public mode, only tools are available let capabilities = ServerCapabilities { tools: Some(ToolsCapability { - list_changed: Some(false), // Tool list is static for now - }), - resources: Some(ResourcesCapability { - subscribe: Some(false), - }), - prompts: Some(PromptsCapability { - list_changed: Some(false), // Prompt list is static + list_changed: Some(false), }), + resources: if self.public_mode { + None + } else { + Some(ResourcesCapability { + subscribe: Some(false), + }) + }, + prompts: if self.public_mode { + None + } else { + Some(PromptsCapability { + list_changed: Some(false), + }) + }, }; // Build server info (version from Cargo.toml) @@ -122,7 +147,7 @@ impl McpServer { debug!("📋 Listing available tools"); - let tools = vec![ + let mut tools = vec![ Tool { name: "discover_instances".to_string(), description: "Discover all running AimDB instances on the system. Scans /tmp/*.sock and /var/run/aimdb/*.sock for AimDB servers.".to_string(), @@ -767,6 +792,11 @@ impl McpServer { }, ]; + // In public mode, only expose the allowlisted tools + if self.public_mode { + tools.retain(|t| PUBLIC_TOOLS.contains(&t.name.as_str())); + } + Ok(ToolsListResult { tools }) } @@ -780,6 +810,33 @@ impl McpServer { debug!("🛠️ Calling tool: {}", params.name); + // Reject non-public tools in public mode (defense in depth) + if self.public_mode && !PUBLIC_TOOLS.contains(¶ms.name.as_str()) { + return Err(McpError::MethodNotFound(format!( + "Unknown tool: {}", + params.name + ))); + } + + // In public mode, strip any client-supplied socket_path so + // resolve_socket_path falls back to the server-pinned --socket flag + // or the AIMDB_SOCKET env var (never a client-chosen path). + // This prevents clients from probing arbitrary Unix sockets on the host. + let arguments = if self.public_mode { + params.arguments.map(|mut v| { + if let Some(obj) = v.as_object_mut() { + obj.remove("socket_path"); + } + v + }) + } else { + params.arguments + }; + let params = ToolCallParams { + name: params.name, + arguments, + }; + let result = match params.name.as_str() { "discover_instances" => tools::discover_instances(params.arguments).await?, "list_records" => tools::list_records(params.arguments).await?, @@ -839,6 +896,9 @@ impl McpServer { if !self.is_ready().await { return Err(McpError::NotInitialized); } + if self.public_mode { + return Err(McpError::MethodNotFound("resources/list".to_string())); + } debug!("📋 Handling resources/list"); resources::list_resources().await @@ -854,6 +914,9 @@ impl McpServer { if !self.is_ready().await { return Err(McpError::NotInitialized); } + if self.public_mode { + return Err(McpError::MethodNotFound("resources/read".to_string())); + } debug!("📖 Handling resources/read: {}", params.uri); resources::read_resource(¶ms.uri).await @@ -866,6 +929,9 @@ impl McpServer { if !self.is_ready().await { return Err(McpError::NotInitialized); } + if self.public_mode { + return Err(McpError::MethodNotFound("prompts/list".to_string())); + } debug!("📋 Listing available prompts"); @@ -884,6 +950,9 @@ impl McpServer { if !self.is_ready().await { return Err(McpError::NotInitialized); } + if self.public_mode { + return Err(McpError::MethodNotFound("prompts/get".to_string())); + } debug!("📝 Getting prompt: {}", params.name); @@ -902,3 +971,159 @@ impl Default for McpServer { Self::new() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn public_tools_allowlist_is_valid() { + // Ensure the allowlist only contains tool names that actually exist in the + // dispatch table, catching typos when tools are renamed. This list is a + // snapshot — keep it in sync with the match arms in handle_tools_call(). + let known_tools = [ + "discover_instances", + "list_records", + "get_record", + "set_record", + "get_instance_info", + "query_schema", + "drain_record", + "graph_nodes", + "graph_edges", + "graph_topo_order", + "get_architecture", + "propose_add_record", + "propose_modify_buffer", + "propose_add_connector", + "propose_modify_fields", + "propose_modify_key_variants", + "propose_add_task", + "propose_add_binary", + "resolve_proposal", + "remove_record", + "rename_record", + "remove_task", + "remove_binary", + "validate_against_instance", + "get_buffer_metrics", + "save_memory", + "reset_session", + ]; + for tool in PUBLIC_TOOLS { + assert!( + known_tools.contains(tool), + "PUBLIC_TOOLS contains unknown tool: {tool}" + ); + } + } + + #[test] + fn public_mode_defaults_to_off() { + let server = McpServer::new(); + assert!(!server.is_public()); + } + + #[test] + fn public_mode_can_be_enabled() { + let server = McpServer::new().with_public_mode(true); + assert!(server.is_public()); + } + + #[tokio::test] + async fn public_mode_filters_tools_list() { + let server = McpServer::new().with_public_mode(true); + server.set_state(ServerState::Ready).await; + let result = server.handle_tools_list().await.unwrap(); + let names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect(); + assert_eq!(names, PUBLIC_TOOLS); + } + + #[tokio::test] + async fn public_mode_rejects_non_public_tool() { + let server = McpServer::new().with_public_mode(true); + server.set_state(ServerState::Ready).await; + let params = ToolCallParams { + name: "set_record".to_string(), + arguments: None, + }; + let err = server.handle_tools_call(params).await.unwrap_err(); + assert!(matches!(err, McpError::MethodNotFound(_))); + } + + // Helper: assert that an explicit socket_path is stripped in public mode. + // The stripping is confirmed by getting InvalidParams (no socket configured) + // rather than a connection error to the attacker-supplied path. + async fn assert_socket_path_stripped(tool: &str) { + // Clear env so it doesn't interfere with the expected InvalidParams result. + std::env::remove_var("AIMDB_SOCKET"); + + let server = McpServer::new().with_public_mode(true); + server.set_state(ServerState::Ready).await; + let params = ToolCallParams { + name: tool.to_string(), + arguments: Some(json!({ "socket_path": "/tmp/evil.sock" })), + }; + let err = server.handle_tools_call(params).await.unwrap_err(); + assert!( + matches!(err, McpError::InvalidParams(_)), + "expected InvalidParams for {tool}, got: {err:?}" + ); + } + + #[tokio::test] + async fn public_mode_strips_socket_path_list_records() { + assert_socket_path_stripped("list_records").await; + } + + #[tokio::test] + async fn public_mode_strips_socket_path_get_record() { + assert_socket_path_stripped("get_record").await; + } + + #[tokio::test] + async fn public_mode_strips_socket_path_discover_instances() { + // discover_instances scans the filesystem directly — it doesn't call + // resolve_socket_path, so stripping socket_path doesn't cause InvalidParams. + // The expected outcome is that the tool runs normally (no instances found in + // the test environment), confirming the evil socket_path was not connected to. + let server = McpServer::new().with_public_mode(true); + server.set_state(ServerState::Ready).await; + let params = ToolCallParams { + name: "discover_instances".to_string(), + arguments: Some(json!({ "socket_path": "/tmp/evil.sock" })), + }; + let result = server.handle_tools_call(params).await; + // The tool is allowed (not MethodNotFound) and does not attempt to connect + // to the evil socket. Either Ok or a no-instances error are both acceptable. + assert!( + !matches!(result, Err(McpError::MethodNotFound(_))), + "discover_instances should not be blocked in public mode" + ); + } + + #[tokio::test] + async fn normal_mode_lists_all_tools() { + let server = McpServer::new(); + server.set_state(ServerState::Ready).await; + let result = server.handle_tools_list().await.unwrap(); + assert!(result.tools.len() > PUBLIC_TOOLS.len()); + } + + #[tokio::test] + async fn public_mode_suppresses_resources_and_prompts() { + let server = McpServer::new().with_public_mode(true); + let params = InitializeParams { + protocol_version: MCP_PROTOCOL_VERSION.to_string(), + capabilities: crate::protocol::ClientCapabilities { sampling: None }, + client_info: crate::protocol::ClientInfo { + name: "test".to_string(), + version: "0.1".to_string(), + }, + }; + let result = server.handle_initialize(params).await.unwrap(); + assert!(result.capabilities.tools.is_some()); + assert!(result.capabilities.resources.is_none()); + assert!(result.capabilities.prompts.is_none()); + } +} diff --git a/tools/aimdb-mcp/src/tools/mod.rs b/tools/aimdb-mcp/src/tools/mod.rs index 8ac402db..7a8eecbb 100644 --- a/tools/aimdb-mcp/src/tools/mod.rs +++ b/tools/aimdb-mcp/src/tools/mod.rs @@ -14,26 +14,36 @@ pub mod schema; // Global connection pool (initialized once) static CONNECTION_POOL: OnceCell = OnceCell::new(); +// Default socket path set by --socket at startup (takes precedence over AIMDB_SOCKET env var) +static DEFAULT_SOCKET: OnceCell = OnceCell::new(); + /// Initialize the connection pool for tools pub fn init_connection_pool(pool: ConnectionPool) { CONNECTION_POOL.set(pool).ok(); } +/// Set the default socket path (called once at startup from --socket flag). +pub fn set_default_socket(path: String) { + DEFAULT_SOCKET.set(path).ok(); +} + /// Get the connection pool pub(crate) fn connection_pool() -> Option<&'static ConnectionPool> { CONNECTION_POOL.get() } -/// Resolve the socket path from an explicit argument or the `AIMDB_SOCKET` env var. +/// Resolve the socket path from an explicit argument, the `--socket` flag, or +/// the `AIMDB_SOCKET` env var (checked in that order). /// -/// Returns an error if neither is set. +/// Returns an error if none are set. pub(crate) fn resolve_socket_path(explicit: Option) -> crate::error::McpResult { explicit + .or_else(|| DEFAULT_SOCKET.get().cloned()) .or_else(|| std::env::var("AIMDB_SOCKET").ok()) .filter(|s| !s.is_empty()) .ok_or_else(|| { crate::error::McpError::InvalidParams( - "Missing socket_path (pass it explicitly or set AIMDB_SOCKET env var)".into(), + "Missing socket_path (pass it explicitly, use --socket, or set AIMDB_SOCKET env var)".into(), ) }) }