From 244e1410533d9aba051691832bc2b0b49247d3d5 Mon Sep 17 00:00:00 2001 From: Anthony Merlo Date: Thu, 20 Aug 2026 16:52:11 +0100 Subject: [PATCH] feat: scaffold nutrition-mcp server with all 13 tools Rust MCP server using rmcp SDK with streamable HTTP transport. - Config from env vars (bind, api_key, db_path, off_user_agent) - SQLite schema: products (FTS5), entries, daily_goal, weight_log - OFF API client: Search-a-licious + v3 barcode + SQLite caching - 13 MCP tools: search_food, get_food_by_barcode, log_food, log_custom_food, delete_entry, daily_summary, history, list_entries, get_goals, set_goals, log_weight, weight_history, bulk_import (stub) - 14 tracked nutrients (gout + hypertension aware) - Bearer token auth middleware - Smoke tested: health, initialize, tools/list, get_goals, daily_summary Closes nutrition-mcp-dfl --- .beads/interactions.jsonl | 1 + .gitignore | 10 +- Cargo.lock | 2874 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 49 + src/api.rs | 241 ++++ src/config.rs | 74 + src/db.rs | 183 +++ src/main.rs | 110 ++ src/tools.rs | 823 +++++++++++ 9 files changed, 4360 insertions(+), 5 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/api.rs create mode 100644 src/config.rs create mode 100644 src/db.rs create mode 100644 src/main.rs create mode 100644 src/tools.rs diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index e69de29..2502259 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -0,0 +1 @@ +{"id":"int-318d614e9de852a6fde2cb00ed524b17","kind":"field_change","created_at":"2026-08-20T15:52:06.000603613Z","actor":"Anthony Merlo","issue_id":"nutrition-mcp-dfl","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Scaffold complete. Cargo.toml + 4 modules (main, config, db, api, tools). Binary compiles and passes smoke test: health check, MCP initialize, 13 tools listed, get_goals returns correct defaults, daily_summary works, auth middleware blocks wrong key."}} diff --git a/.gitignore b/.gitignore index 69dff11..0c5f072 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ - -# Beads / Dolt files (added by bd init) -.dolt/ +/target *.db -.beads-credential-key -.beads/proxieddb/ +*.db-journal +*.db-wal +*.db-shm +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bf46aa5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2874 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +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 = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.9.2", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.7", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "nutrition-mcp" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "chrono", + "reqwest", + "rmcp", + "schemars", + "serde", + "serde_json", + "sqlx", + "thiserror", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c93da5bb2c5d4e6c0ef7abeead62c89169a0a4882bfb83ac892f2423aea2fe" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rmcp" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534fd1cd0601e798ac30545ff2b7f4a62c6f14edd4aaed1cc5eb1e85f69f09af" +dependencies = [ + "base64", + "bytes", + "chrono", + "futures", + "http", + "http-body", + "http-body-util", + "paste", + "pin-project-lite", + "rand 0.9.5", + "rmcp-macros", + "schemars", + "serde", + "serde_json", + "sse-stream", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tower-service", + "tracing", + "uuid", +] + +[[package]] +name = "rmcp-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba777eb0e5f53a757e36f0e287441da0ab766564ba7201600eeb92a4753022e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.119", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "chrono", + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.3", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.119", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.119", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.7", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.7", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "sse-stream" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c123f296ade4ec4b8b0f6162116e6629f5146922ca5ab40ca9d3c2e73ab4761e" +dependencies = [ + "bytes", + "futures-util", + "http-body", + "http-body-util", + "pin-project-lite", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +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.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1f3d1ca --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "nutrition-mcp" +version = "0.1.0" +edition = "2024" +description = "MCP server for personal calorie/nutrient tracking with OpenFood Facts integration" +license = "MIT" + +[dependencies] +# MCP protocol +rmcp = { version = "0.7", features = [ + "server", + "macros", + "schemars", + "transport-streamable-http-server", +] } + +# Async runtime +tokio = { version = "1", features = ["full"] } +tokio-util = { version = "0.7", features = ["rt"] } + +# HTTP client (for OFF API) +reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } + +# Database +sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "macros"] } + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" +schemars = "1" + +# Web framework (for HTTP transport + auth middleware) +axum = "0.8" + +# Error handling +anyhow = "1" +thiserror = "2" + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Misc +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1", features = ["v4"] } + +[profile.release] +opt-level = 2 +strip = true \ No newline at end of file diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..77f5472 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,241 @@ +// OpenFood Facts API client: Search-a-licious + v3 barcode + SQLite caching. + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::db::DbPool; + +/// OFF API client for food lookups. +#[derive(Clone)] +pub struct OffClient { + http: reqwest::Client, + user_agent: String, +} + +/// A cached product from OFF (stored in SQLite products table). +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] +pub struct Product { + pub code: String, + pub product_name: String, + pub brands: Option, + pub nutriments: String, // JSON blob + pub serving_quantity: Option, + pub serving_size: Option, + pub image_url: Option, +} + +/// Search result from Search-a-licious (simplified). +#[derive(Debug, Deserialize)] +struct SearchResponse { + hits: Vec, +} + +#[derive(Debug, Deserialize)] +struct SearchHit { + #[serde(default)] + code: Option, + #[serde(default)] + product_name: Option, + #[serde(default)] + brands: Option, + #[serde(default)] + nutriments: Option, + #[serde(default)] + serving_quantity: Option, + #[serde(default)] + serving_size: Option, + #[serde(default)] + image_url: Option, +} + +/// OFF API v3 product response (simplified). +#[derive(Debug, Deserialize)] +struct V3ProductResponse { + product: V3Product, +} + +#[derive(Debug, Deserialize)] +struct V3Product { + code: String, + #[serde(default)] + product_name: Option, + #[serde(default)] + brands: Option, + #[serde(default)] + nutriments: Option, + #[serde(default)] + serving_quantity: Option, + #[serde(default)] + serving_size: Option, + #[serde(default)] + image_url: Option, +} + +impl OffClient { + pub fn new(user_agent: &str) -> Self { + let http = reqwest::Client::builder() + .user_agent(user_agent) + .timeout(std::time::Duration::from_secs(15)) + .build() + .expect("failed to build reqwest client"); + + Self { + http, + user_agent: user_agent.to_string(), + } + } + + /// Search products by name using Search-a-licious. + /// Returns up to `page_size` results. + pub async fn search( + &self, + query: &str, + page_size: u32, + ) -> Result> { + let url = "https://search.openfoodfacts.org/api/v1/search"; + let params = [ + ("q", query.to_string()), + ("page_size", page_size.to_string()), + ("langs", "en".to_string()), + ( + "fields", + "code,product_name,brands,nutriments,serving_quantity,serving_size,image_url" + .to_string(), + ), + ]; + + tracing::debug!("OFF search: {} (limit {})", query, page_size); + + let resp: SearchResponse = self + .http + .get(url) + .query(¶ms) + .send() + .await + .context("OFF search request failed")? + .error_for_status()? + .json() + .await + .context("failed to parse OFF search response")?; + + let products = resp + .hits + .into_iter() + .filter_map(|h| { + let name = h.product_name.clone().unwrap_or_else(|| "Unknown".to_string()); + Some(Product { + code: h.code?, + product_name: name, + brands: h.brands, + nutriments: h + .nutriments + .map(|v| v.to_string()) + .unwrap_or_else(|| "{}".to_string()), + serving_quantity: h.serving_quantity, + serving_size: h.serving_size, + image_url: h.image_url, + }) + }) + .collect(); + + Ok(products) + } + + /// Get a single product by barcode using OFF API v3. + pub async fn get_by_barcode(&self, barcode: &str) -> Result> { + let url = format!( + "https://world.openfoodfacts.org/api/v3/product/{}", + barcode + ); + let params = [ + ( + "fields", + "code,product_name,brands,nutriments,serving_quantity,serving_size,image_url", + ), + ]; + + tracing::debug!("OFF barcode lookup: {}", barcode); + + let resp = self.http.get(&url).query(¶ms).send().await; + + match resp { + Ok(r) if r.status().is_success() => { + let v3: V3ProductResponse = + r.json().await.context("failed to parse OFF v3 response")?; + let p = v3.product; + Ok(Some(Product { + code: p.code, + product_name: p.product_name.unwrap_or_else(|| "Unknown".to_string()), + brands: p.brands, + nutriments: p + .nutriments + .map(|v| v.to_string()) + .unwrap_or_else(|| "{}".to_string()), + serving_quantity: p.serving_quantity, + serving_size: p.serving_size, + image_url: p.image_url, + })) + } + Ok(r) if r.status().as_u16() == 404 => Ok(None), + Ok(r) => anyhow::bail!("OFF API error: {}", r.status()), + Err(e) => Err(e).context("OFF barcode request failed"), + } + } + + /// Cache products in SQLite. Upserts by code. + pub async fn cache_products(pool: &DbPool, products: &[Product]) -> Result<()> { + for p in products { + sqlx::query( + r#"INSERT INTO products (code, product_name, brands, nutriments, serving_quantity, serving_size, image_url, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now')) + ON CONFLICT(code) DO UPDATE SET + product_name = excluded.product_name, + brands = excluded.brands, + nutriments = excluded.nutriments, + serving_quantity = excluded.serving_quantity, + serving_size = excluded.serving_size, + image_url = excluded.image_url, + updated_at = datetime('now')"#, + ) + .bind(&p.code) + .bind(&p.product_name) + .bind(&p.brands) + .bind(&p.nutriments) + .bind(p.serving_quantity) + .bind(&p.serving_size) + .bind(&p.image_url) + .execute(pool) + .await?; + } + Ok(()) + } + + /// Look up a product in the SQLite cache by barcode. + pub async fn cache_get(pool: &DbPool, code: &str) -> Result> { + let product = sqlx::query_as::<_, Product>( + r#"SELECT code, product_name, brands, nutriments, serving_quantity, serving_size, image_url + FROM products WHERE code = ?"#, + ) + .bind(code) + .fetch_optional(pool) + .await?; + Ok(product) + } + + /// Search the SQLite cache by product name using FTS5. + pub async fn cache_search(pool: &DbPool, query: &str, limit: u32) -> Result> { + let products = sqlx::query_as::<_, Product>( + r#"SELECT p.code, p.product_name, p.brands, p.nutriments, p.serving_quantity, p.serving_size, p.image_url + FROM products_fts fts + JOIN products p ON p.rowid = fts.rowid + WHERE products_fts MATCH ? + LIMIT ?"#, + ) + .bind(query) + .bind(limit) + .fetch_all(pool) + .await?; + Ok(products) + } +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..9cac45a --- /dev/null +++ b/src/config.rs @@ -0,0 +1,74 @@ +use std::path::PathBuf; +use anyhow::{Result, bail}; + +/// Server configuration loaded from environment variables. +#[derive(Debug, Clone)] +pub struct Config { + /// Bind address, e.g. "0.0.0.0:9432" + pub bind: String, + /// API key for Bearer auth + pub api_key: String, + /// Path to SQLite database file + pub db_path: PathBuf, + /// User-Agent header for OpenFood Facts API requests + pub off_user_agent: String, +} + +impl Config { + pub fn from_env() -> Result { + let bind = std::env::var("NUTRITION_MCP_BIND") + .unwrap_or_else(|_| "0.0.0.0:9432".to_string()); + + let api_key = std::env::var("NUTRITION_MCP_API_KEY") + .unwrap_or_else(|_| { + tracing::warn!("NUTRITION_MCP_API_KEY not set — generating a random key"); + uuid::Uuid::new_v4().to_string() + }); + + let db_path = std::env::var("NUTRITION_MCP_DB_PATH") + .map(PathBuf::from) + .unwrap_or_else(|_| { + let mut p = dirs_data_path(); + p.push("nutrition"); + p.push("nutrition.db"); + p + }); + + let off_user_agent = std::env::var("NUTRITION_MCP_OFF_USER_AGENT") + .unwrap_or_else(|_| { + "NutritionMCP/0.1.0 (personal use)".to_string() + }); + + if api_key.is_empty() { + bail!("NUTRITION_MCP_API_KEY must not be empty"); + } + + Ok(Self { + bind, + api_key, + db_path, + off_user_agent, + }) + } + + /// Create the parent directory of db_path if it doesn't exist. + pub fn ensure_db_dir(&self) -> Result<()> { + if let Some(parent) = self.db_path.parent() { + std::fs::create_dir_all(parent)?; + } + Ok(()) + } +} + +fn dirs_data_path() -> PathBuf { + // Prefer XDG_DATA_HOME, fall back to ~/.local/share + std::env::var("XDG_DATA_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string()); + let mut p = PathBuf::from(home); + p.push(".local"); + p.push("share"); + p + }) +} \ No newline at end of file diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..1243a5c --- /dev/null +++ b/src/db.rs @@ -0,0 +1,183 @@ +// Database layer: SQLite schema, connection pool, and queries. + +use anyhow::Result; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; +use sqlx::SqlitePool; +use std::path::Path; +use std::str::FromStr; + +pub type DbPool = SqlitePool; + +/// 14 tracked nutrients with their OFF field names. +/// Used for daily summaries and goal tracking. +pub const TRACKED_NUTRIENTS: &[(&str, &str)] = &[ + ("energy_kcal", "energy-kcal_100g"), + ("protein", "proteins_100g"), + ("carbohydrates", "carbohydrates_100g"), + ("fat", "fat_100g"), + ("fiber", "fiber_100g"), + ("sugars", "sugars_100g"), + ("saturated_fat", "saturated-fat_100g"), + ("salt", "salt_100g"), + ("fructose", "fructose_100g"), + ("alcohol", "alcohol_100g"), + ("potassium", "potassium_100g"), + ("calcium", "calcium_100g"), + ("magnesium", "magnesium_100g"), + ("cholesterol", "cholesterol_100g"), +]; + +/// Default daily goals (gout + hypertension aware). +/// Values are in grams or mg as appropriate (matches OFF _100g units). +pub const DEFAULT_GOALS_JSON: &str = r#"{ + "energy_kcal": 2500, + "protein": 150, + "carbohydrates": 300, + "fat": 70, + "fiber": 30, + "sugars": 90, + "saturated_fat": 20, + "salt": 6, + "fructose": 15, + "alcohol": 0, + "potassium": 3500, + "calcium": 700, + "magnesium": 300, + "cholesterol": 300 +}"#; + +/// Initialize the SQLite database: create pool, run migrations. +pub async fn init_database(db_path: &Path) -> Result { + let conn_opts = SqliteConnectOptions::from_str(&format!("sqlite://{}", db_path.display()))? + .create_if_missing(true) + .foreign_keys(true) + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal); + + let pool = SqlitePoolOptions::new() + .max_connections(5) + .connect_with(conn_opts) + .await?; + + run_migrations(&pool, db_path).await?; + + Ok(pool) +} + +async fn run_migrations(pool: &DbPool, db_path: &Path) -> Result<()> { + // products — cache of OFF API results + sqlx::query( + r#"CREATE TABLE IF NOT EXISTS products ( + code TEXT PRIMARY KEY, + product_name TEXT NOT NULL, + brands TEXT, + nutriments TEXT NOT NULL DEFAULT '{}', + serving_quantity REAL, + serving_size TEXT, + image_url TEXT, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + )"#, + ) + .execute(pool) + .await?; + + // FTS5 virtual table for fast product name search + sqlx::query( + r#"CREATE VIRTUAL TABLE IF NOT EXISTS products_fts USING fts5( + product_name, + content='products', + content_rowid='rowid' + )"#, + ) + .execute(pool) + .await?; + + // Triggers to keep FTS5 index in sync with products table + sqlx::query( + r#"CREATE TRIGGER IF NOT EXISTS products_ai AFTER INSERT ON products BEGIN + INSERT INTO products_fts(rowid, product_name) VALUES (new.rowid, new.product_name); + END"#, + ) + .execute(pool) + .await?; + + sqlx::query( + r#"CREATE TRIGGER IF NOT EXISTS products_ad AFTER DELETE ON products BEGIN + INSERT INTO products_fts(products_fts, rowid, product_name) VALUES ('delete', old.rowid, old.product_name); + END"#, + ) + .execute(pool) + .await?; + + sqlx::query( + r#"CREATE TRIGGER IF NOT EXISTS products_au AFTER UPDATE ON products BEGIN + INSERT INTO products_fts(products_fts, rowid, product_name) VALUES ('delete', old.rowid, old.product_name); + INSERT INTO products_fts(rowid, product_name) VALUES (new.rowid, new.product_name); + END"#, + ) + .execute(pool) + .await?; + + // entries — food log (one row per food item consumed) + sqlx::query( + r#"CREATE TABLE IF NOT EXISTS entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + meal TEXT NOT NULL, + product_code TEXT, + food_name TEXT NOT NULL, + grams REAL, + servings REAL, + nutriments TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )"#, + ) + .execute(pool) + .await?; + + sqlx::query( + r#"CREATE INDEX IF NOT EXISTS idx_entries_date ON entries(date)"#, + ) + .execute(pool) + .await?; + + // daily_goal — single row (id=1), JSON column with all nutrient targets + sqlx::query( + r#"CREATE TABLE IF NOT EXISTS daily_goal ( + id INTEGER PRIMARY KEY DEFAULT 1, + goals TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + CHECK (id = 1) + )"#, + ) + .execute(pool) + .await?; + + // Seed default goals if not present + sqlx::query( + r#"INSERT OR IGNORE INTO daily_goal (id, goals) VALUES (1, ?)"#, + ) + .bind(DEFAULT_GOALS_JSON) + .execute(pool) + .await?; + + // weight_log — body weight tracking + sqlx::query( + r#"CREATE TABLE IF NOT EXISTS weight_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + weight_kg REAL NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )"#, + ) + .execute(pool) + .await?; + + sqlx::query( + r#"CREATE INDEX IF NOT EXISTS idx_weight_log_date ON weight_log(date)"#, + ) + .execute(pool) + .await?; + + tracing::info!("Database initialized at {}", db_path.display()); + Ok(()) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0fc2f36 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,110 @@ +//nutrition-mcp: MCP server for personal nutrition tracking +// +//Provides 12 tools for food lookup (OpenFood Facts), meal logging, +//daily summaries, goal management, and weight tracking. +//Runs as a streamable HTTP server with API key auth. + +mod api; +mod config; +mod db; +mod tools; + +use std::net::SocketAddr; +use std::sync::Arc; + +use anyhow::Result; +use axum::{ + Router, + extract::State, + http::{HeaderMap, Request, StatusCode}, + middleware::{self, Next}, + response::Response, + routing::get, +}; +use rmcp::transport::{ + StreamableHttpServerConfig, + streamable_http_server::{session::local::LocalSessionManager, tower::StreamableHttpService}, +}; + +use config::Config; +use tools::NutritionServer; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "info".into()), + ) + .init(); + + let config = Config::from_env()?; + tracing::info!("Nutrition MCP server starting on {}", config.bind); + + // Ensure data directory and database exist + config.ensure_db_dir()?; + db::init_database(&config.db_path).await?; + + let config = Arc::new(config); + + // Create the MCP service — each session gets a fresh NutritionServer + let config_for_factory = config.clone(); + let mcp_service: StreamableHttpService = + StreamableHttpService::new( + move || { + Ok(NutritionServer::new(config_for_factory.clone())) + }, + LocalSessionManager::default().into(), + StreamableHttpServerConfig::default(), + ); + + // Auth middleware — checks Authorization: Bearer + let api_key = config.api_key.clone(); + let protected_mcp = Router::new() + .nest_service("/mcp", mcp_service) + .layer(middleware::from_fn_with_state( + api_key, + auth_middleware, + )); + + let app = Router::new() + .route("/health", get(health_check)) + .merge(protected_mcp); + + let addr: SocketAddr = config.bind.parse()?; + let listener = tokio::net::TcpListener::bind(addr).await?; + tracing::info!("Server listening on http://{}", addr); + + axum::serve(listener, app) + .with_graceful_shutdown(async { + tokio::signal::ctrl_c().await.ok(); + tracing::info!("Shutting down..."); + }) + .await?; + + Ok(()) +} + +/// Health check endpoint — no auth required +async fn health_check() -> &'static str { + "OK" +} + +/// Bearer token auth middleware +async fn auth_middleware( + State(expected_key): State, + headers: HeaderMap, + request: Request, + next: Next, +) -> Result { + let token = headers + .get("Authorization") + .and_then(|v| v.to_str().ok()) + .and_then(|h| h.strip_prefix("Bearer ")) + .map(|t| t.trim().to_string()); + + match token { + Some(t) if t == expected_key => Ok(next.run(request).await), + _ => Err(StatusCode::UNAUTHORIZED), + } +} \ No newline at end of file diff --git a/src/tools.rs b/src/tools.rs new file mode 100644 index 0000000..41cbc77 --- /dev/null +++ b/src/tools.rs @@ -0,0 +1,823 @@ +// MCP tools: all 12 tools exposed to MCP clients. +// +// Food lookup: search_food, get_food_by_barcode +// Logging: log_food, log_custom_food, delete_entry +// Summary: daily_summary, history, list_entries +// Goals: get_goals, set_goals +// Weight: log_weight, weight_history +// Future: bulk_import (stub) + +use std::sync::Arc; + +use rmcp::{ + ErrorData as McpError, ServerHandler, + handler::server::{ + router::tool::ToolRouter, + wrapper::Parameters, + }, + model::*, + schemars, + tool, tool_handler, tool_router, +}; +use serde::Deserialize; +use serde_json::{json, Value}; +use sqlx::Row; + +use crate::api::{OffClient, Product}; +use crate::config::Config; +use crate::db::{self, TRACKED_NUTRIENTS}; + +/// The MCP server handler. Each client session gets its own instance, +/// but they share the same Config (which contains the DB path — the pool +/// is created per-session to keep the struct Clone-able). +#[derive(Clone)] +pub struct NutritionServer { + config: Arc, + off_client: OffClient, + tool_router: ToolRouter, +} + +impl NutritionServer { + pub fn new(config: Arc) -> Self { + let off_client = OffClient::new(&config.off_user_agent); + Self { + config, + off_client, + tool_router: Self::tool_router(), + } + } + + /// Get a DB pool. Creates one on demand (cheap — SQLite is file-based). + async fn pool(&self) -> Result { + db::init_database(&self.config.db_path) + .await + .map_err(|e| McpError::internal_error(format!("DB init failed: {e}"), None)) + } +} + +// ── Tool parameter structs ────────────────────────────────────────── + +#[derive(Debug, Deserialize, schemars::JsonSchema)] +pub struct SearchFoodParams { + /// Food name to search for (e.g. "chicken breast", "coca cola") + pub query: String, + /// Number of results to return (default 10, max 50) + #[serde(default)] + pub page_size: Option, +} + +#[derive(Debug, Deserialize, schemars::JsonSchema)] +pub struct BarcodeParams { + /// Product barcode (EAN-13, UPC, etc.) + pub barcode: String, +} + +#[derive(Debug, Deserialize, schemars::JsonSchema)] +pub struct LogFoodParams { + /// Product code (barcode) from OFF — use search_food or get_food_by_barcode first + pub product_code: String, + /// Amount in grams. If provided, takes precedence over servings. + #[serde(default)] + pub grams: Option, + /// Number of servings. Uses OFF serving_quantity if available. + #[serde(default)] + pub servings: Option, + /// Date in YYYY-MM-DD format. Defaults to today. + #[serde(default)] + pub date: Option, + /// Meal type: breakfast, lunch, dinner, snack. Auto-detected from time if omitted. + #[serde(default)] + pub meal: Option, +} + +#[derive(Debug, Deserialize, schemars::JsonSchema)] +pub struct LogCustomFoodParams { + /// Food name (e.g. "homemade salad", "restaurant pizza") + pub name: String, + /// Calories per 100g + pub calories: f64, + /// Nutrients per 100g as JSON object. Keys: protein, carbohydrates, fat, fiber, sugars, + /// saturated_fat, salt, fructose, alcohol, potassium, calcium, magnesium, cholesterol. + /// All values in grams or mg (matching OFF _100g units). Calories separate. + #[serde(default)] + pub nutriments: Option, + /// Amount consumed in grams + pub grams: f64, + /// Date in YYYY-MM-DD format. Defaults to today. + #[serde(default)] + pub date: Option, + /// Meal type. Auto-detected from time if omitted. + #[serde(default)] + pub meal: Option, +} + +#[derive(Debug, Deserialize, schemars::JsonSchema)] +pub struct DeleteEntryParams { + /// Entry ID (from list_entries) + pub id: i64, +} + +#[derive(Debug, Deserialize, schemars::JsonSchema)] +pub struct DateParams { + /// Date in YYYY-MM-DD format. Defaults to today. + #[serde(default)] + pub date: Option, +} + +#[derive(Debug, Deserialize, schemars::JsonSchema)] +pub struct HistoryParams { + /// Number of days of history (default 7) + #[serde(default)] + pub days: Option, +} + +#[derive(Debug, Deserialize, schemars::JsonSchema)] +pub struct SetGoalsParams { + /// JSON object with nutrient goals. Partial updates are merged with existing goals. + /// Keys: energy_kcal, protein, carbohydrates, fat, fiber, sugars, saturated_fat, + /// salt, fructose, alcohol, potassium, calcium, magnesium, cholesterol. + pub goals: Value, +} + +#[derive(Debug, Deserialize, schemars::JsonSchema)] +pub struct LogWeightParams { + /// Body weight in kilograms + pub weight_kg: f64, + /// Date in YYYY-MM-DD format. Defaults to today. + #[serde(default)] + pub date: Option, +} + +#[derive(Debug, Deserialize, schemars::JsonSchema)] +pub struct WeightHistoryParams { + /// Number of days of weight history (default 30) + #[serde(default)] + pub days: Option, +} + +#[derive(Debug, Deserialize, schemars::JsonSchema)] +pub struct BulkImportParams { + /// URL to a Parquet file (future: HuggingFace dataset). Not yet implemented. + #[serde(default)] + pub parquet_url: Option, +} + +// ── Helper functions ──────────────────────────────────────────────── + +fn today() -> String { + chrono::Local::now().format("%Y-%m-%d").to_string() +} + +fn detect_meal() -> String { + let hour = chrono::Local::now().format("%H").to_string().parse::().unwrap_or(12); + match hour { + h if h < 11 => "breakfast".to_string(), + h if h < 15 => "lunch".to_string(), + h if h < 21 => "dinner".to_string(), + _ => "snack".to_string(), + } +} + +/// Scale nutrient JSON from per-100g to actual portion. +/// multiplies each numeric value by (grams / 100). +fn scale_nutriments(nutriments: &Value, grams: f64) -> Value { + let factor = grams / 100.0; + if let Some(obj) = nutriments.as_object() { + let scaled: serde_json::Map = obj + .iter() + .map(|(k, v)| { + let scaled_v = if let Some(n) = v.as_f64() { + json!(n * factor) + } else { + v.clone() + }; + (k.clone(), scaled_v) + }) + .collect(); + Value::Object(scaled) + } else { + nutriments.clone() + } +} + +/// Build a summary JSON from a list of entries' nutriments. +/// Sums all tracked nutrients, returns "no data" for missing ones. +fn sum_nutriments(entries_nutriments: &[Value]) -> Value { + let mut totals: serde_json::Map = serde_json::Map::new(); + + for (_, tracked_key) in TRACKED_NUTRIENTS { + let mut sum = 0.0; + let mut has_data = false; + for entry_nutriments in entries_nutriments { + if let Some(val) = entry_nutriments.get(tracked_key).and_then(|v| v.as_f64()) { + sum += val; + has_data = true; + } + } + let display_key = tracked_key.replace("_100g", ""); + totals.insert( + display_key, + if has_data { json!(sum) } else { json!("no data") }, + ); + } + + Value::Object(totals) +} + +/// Format a product for display in tool results. +fn product_to_json(p: &Product) -> Value { + let nutriments: Value = serde_json::from_str(&p.nutriments).unwrap_or(json!({})); + json!({ + "code": p.code, + "product_name": p.product_name, + "brands": p.brands, + "nutriments_per_100g": nutriments, + "serving_quantity": p.serving_quantity, + "serving_size": p.serving_size, + "image_url": p.image_url, + }) +} + +fn ok(text: String) -> Result { + Ok(CallToolResult::success(vec![Content::text(text)])) +} + +// ── MCP tool implementations ──────────────────────────────────────── + +#[tool_router] +impl NutritionServer { + // ── Food lookup ────────────────────────────────────────────── + + #[tool( + description = "Search for food products by name using OpenFood Facts full-text search. \ + Returns products with nutrition data per 100g. Results are cached locally. \ + Use this when the user mentions a food item by name." + )] + async fn search_food( + &self, + Parameters(params): Parameters, + ) -> Result { + let pool = self.pool().await?; + let page_size = params.page_size.unwrap_or(10).min(50); + + // Try cache first (FTS5) + let cached = OffClient::cache_search(&pool, ¶ms.query, page_size) + .await + .map_err(|e| McpError::internal_error(format!("Cache search failed: {e}"), None))?; + + let products = if !cached.is_empty() { + tracing::debug!("Cache hit for '{}': {} results", params.query, cached.len()); + cached + } else { + tracing::debug!("Cache miss for '{}', querying OFF", params.query); + let results = self + .off_client + .search(¶ms.query, page_size) + .await + .map_err(|e| McpError::internal_error(format!("OFF search failed: {e}"), None))?; + + // Cache the results + let _ = OffClient::cache_products(&pool, &results).await; + results + }; + + if products.is_empty() { + return ok(format!("No products found for \"{}\"", params.query)); + } + + let results: Vec = products.iter().map(product_to_json).collect(); + ok(serde_json::to_string_pretty(&json!({ + "query": params.query, + "count": results.len(), + "products": results + })) + .unwrap_or_default()) + } + + #[tool( + description = "Look up a food product by barcode (EAN-13, UPC) using OpenFood Facts API v3. \ + Returns full nutrition data. Result is cached locally." + )] + async fn get_food_by_barcode( + &self, + Parameters(params): Parameters, + ) -> Result { + let pool = self.pool().await?; + + // Check cache + if let Some(cached) = OffClient::cache_get(&pool, ¶ms.barcode) + .await + .map_err(|e| McpError::internal_error(format!("Cache lookup failed: {e}"), None))? + { + return ok(serde_json::to_string_pretty(&product_to_json(&cached)).unwrap_or_default()); + } + + // Query OFF API + let product = self + .off_client + .get_by_barcode(¶ms.barcode) + .await + .map_err(|e| McpError::internal_error(format!("OFF API failed: {e}"), None))?; + + match product { + Some(p) => { + let _ = OffClient::cache_products(&pool, std::slice::from_ref(&p)).await; + ok(serde_json::to_string_pretty(&product_to_json(&p)).unwrap_or_default()) + } + None => ok(format!( + "No product found for barcode {}", + params.barcode + )), + } + } + + // ── Logging ────────────────────────────────────────────────── + + #[tool( + description = "Log a food item from OpenFood Facts by product code (barcode). \ + Scales nutrients by portion (grams or servings). \ + Shows daily summary after logging. \ + Always search_food or get_food_by_barcode first to get the product_code." + )] + async fn log_food( + &self, + Parameters(params): Parameters, + ) -> Result { + let pool = self.pool().await?; + let date = params.date.unwrap_or_else(today); + let meal = params.meal.unwrap_or_else(detect_meal); + + // Get product from cache or OFF + let product = if let Some(p) = OffClient::cache_get(&pool, ¶ms.product_code) + .await + .map_err(|e| McpError::internal_error(format!("Cache lookup failed: {e}"), None))? + { + p + } else { + self.off_client + .get_by_barcode(¶ms.product_code) + .await + .map_err(|e| McpError::internal_error(format!("OFF API failed: {e}"), None))? + .ok_or_else(|| { + McpError::invalid_params( + format!("Product not found: {}", params.product_code), + None, + ) + })? + }; + + // Calculate grams + let grams = if let Some(g) = params.grams { + g + } else if let Some(s) = params.servings { + s * product.serving_quantity.unwrap_or(100.0) + } else { + 100.0 // default to 100g + }; + + // Scale nutriments + let nutriments: Value = serde_json::from_str(&product.nutriments).unwrap_or(json!({})); + let scaled = scale_nutriments(&nutriments, grams); + + // Insert entry + let result = sqlx::query( + r#"INSERT INTO entries (date, meal, product_code, food_name, grams, servings, nutriments) + VALUES (?, ?, ?, ?, ?, ?, ?)"#, + ) + .bind(&date) + .bind(&meal) + .bind(¶ms.product_code) + .bind(&product.product_name) + .bind(grams) + .bind(params.servings) + .bind(scaled.to_string()) + .execute(&pool) + .await + .map_err(|e| McpError::internal_error(format!("Insert failed: {e}"), None))?; + + let id = result.last_insert_rowid(); + + // Get daily summary + let summary = self.build_daily_summary(&pool, &date).await?; + + ok(format!( + "✓ Logged: {} ({}g)\n Entry ID: {}\n Meal: {}\n Date: {}\n\n{}", + product.product_name, grams, id, meal, date, summary + )) + } + + #[tool( + description = "Log a custom food item not in OpenFood Facts (homemade meals, restaurant food). \ + Provide calories and nutrients per 100g, plus grams consumed." + )] + async fn log_custom_food( + &self, + Parameters(params): Parameters, + ) -> Result { + let pool = self.pool().await?; + let date = params.date.unwrap_or_else(today); + let meal = params.meal.unwrap_or_else(detect_meal); + + // Build nutriments JSON — include calories in the energy-kcal_100g field + let mut nutriments = params.nutriments.unwrap_or(json!({})); + if let Some(obj) = nutriments.as_object_mut() { + obj.insert("energy-kcal_100g".to_string(), json!(params.calories)); + } else { + nutriments = json!({"energy-kcal_100g": params.calories}); + } + + let scaled = scale_nutriments(&nutriments, params.grams); + + let result = sqlx::query( + r#"INSERT INTO entries (date, meal, food_name, grams, nutriments) + VALUES (?, ?, ?, ?, ?)"#, + ) + .bind(&date) + .bind(&meal) + .bind(¶ms.name) + .bind(params.grams) + .bind(scaled.to_string()) + .execute(&pool) + .await + .map_err(|e| McpError::internal_error(format!("Insert failed: {e}"), None))?; + + let id = result.last_insert_rowid(); + let summary = self.build_daily_summary(&pool, &date).await?; + + ok(format!( + "✓ Logged: {} ({}g, {} cal/100g)\n Entry ID: {}\n Meal: {}\n Date: {}\n\n{}", + params.name, params.grams, params.calories, id, meal, date, summary + )) + } + + #[tool( + description = "Delete a food log entry by ID. Use list_entries to find the ID first." + )] + async fn delete_entry( + &self, + Parameters(params): Parameters, + ) -> Result { + let pool = self.pool().await?; + + let result = sqlx::query("DELETE FROM entries WHERE id = ?") + .bind(params.id) + .execute(&pool) + .await + .map_err(|e| McpError::internal_error(format!("Delete failed: {e}"), None))?; + + if result.rows_affected() == 0 { + return ok(format!("No entry found with ID {}", params.id)); + } + + ok(format!("✓ Deleted entry {}", params.id)) + } + + // ── Summary & History ──────────────────────────────────────── + + #[tool( + description = "Show daily nutrition summary for a date (default: today). \ + Shows all 14 tracked nutrients vs goals, remaining calories/fructose/salt, \ + and a list of all entries for the day." + )] + async fn daily_summary( + &self, + Parameters(params): Parameters, + ) -> Result { + let pool = self.pool().await?; + let date = params.date.unwrap_or_else(today); + let summary = self.build_daily_summary(&pool, &date).await?; + ok(summary) + } + + #[tool( + description = "Show nutrition history for the last N days (default 7). \ + Shows daily totals for key nutrients (calories, protein, fructose, salt, potassium)." + )] + async fn history( + &self, + Parameters(params): Parameters, + ) -> Result { + let pool = self.pool().await?; + let days = params.days.unwrap_or(7); + + let rows = sqlx::query( + r#"SELECT date, nutriments FROM entries + WHERE date >= date('now', ?) + ORDER BY date DESC"#, + ) + .bind(format!("-{} days", days)) + .fetch_all(&pool) + .await + .map_err(|e| McpError::internal_error(format!("History query failed: {e}"), None))?; + + // Group by date + let mut by_date: std::collections::BTreeMap> = std::collections::BTreeMap::new(); + for row in rows { + let date: String = row.try_get("date").map_err(|e| McpError::internal_error(e.to_string(), None))?; + let nutriments_str: String = row.try_get("nutriments").map_err(|e| McpError::internal_error(e.to_string(), None))?; + let n: Value = serde_json::from_str(&nutriments_str).unwrap_or(json!({})); + by_date.entry(date).or_default().push(n); + } + + let mut days_summary = Vec::new(); + for (date, entries) in by_date.iter().rev() { + let totals = sum_nutriments(entries); + days_summary.push(json!({ + "date": date, + "totals": totals, + "entries": entries.len(), + })); + } + + ok(serde_json::to_string_pretty(&json!({ + "days": days, + "history": days_summary + })) + .unwrap_or_default()) + } + + #[tool( + description = "List all food log entries for a date (default: today). \ + Shows entry IDs needed for delete_entry." + )] + async fn list_entries( + &self, + Parameters(params): Parameters, + ) -> Result { + let pool = self.pool().await?; + let date = params.date.unwrap_or_else(today); + + let rows = sqlx::query( + r#"SELECT id, meal, food_name, grams, nutriments, created_at + FROM entries WHERE date = ? ORDER BY created_at"#, + ) + .bind(&date) + .fetch_all(&pool) + .await + .map_err(|e| McpError::internal_error(format!("List query failed: {e}"), None))?; + + if rows.is_empty() { + return ok(format!("No entries for {}", date)); + } + + let mut entries = Vec::new(); + for row in rows { + let id: i64 = row.try_get("id").map_err(|e| McpError::internal_error(e.to_string(), None))?; + let meal: String = row.try_get("meal").map_err(|e| McpError::internal_error(e.to_string(), None))?; + let food_name: String = row.try_get("food_name").map_err(|e| McpError::internal_error(e.to_string(), None))?; + let grams: Option = row.try_get::, _>("grams").ok().flatten(); + let created_at: String = row.try_get("created_at").map_err(|e| McpError::internal_error(e.to_string(), None))?; + + entries.push(json!({ + "id": id, + "meal": meal, + "food_name": food_name, + "grams": grams, + "time": created_at, + })); + } + + ok(serde_json::to_string_pretty(&json!({ + "date": date, + "entries": entries + })) + .unwrap_or_default()) + } + + // ── Goals ──────────────────────────────────────────────────── + + #[tool( + description = "Get current daily nutrition goals. Returns all 14 tracked nutrient targets." + )] + async fn get_goals(&self) -> Result { + let pool = self.pool().await?; + + let row = sqlx::query("SELECT goals FROM daily_goal WHERE id = 1") + .fetch_one(&pool) + .await + .map_err(|e| McpError::internal_error(format!("Goals query failed: {e}"), None))?; + + let goals_str: String = row.try_get("goals").map_err(|e| McpError::internal_error(e.to_string(), None))?; + let goals: Value = serde_json::from_str(&goals_str).unwrap_or(json!({})); + + ok(serde_json::to_string_pretty(&json!({ + "daily_goals": goals, + "tracked_nutrients": TRACKED_NUTRIENTS.iter().map(|(display, off)| { + json!({"display": display, "off_field": off}) + }).collect::>() + })) + .unwrap_or_default()) + } + + #[tool( + description = "Set daily nutrition goals. Accepts partial updates (merged with existing). \ + Keys: energy_kcal, protein, carbohydrates, fat, fiber, sugars, saturated_fat, \ + salt, fructose, alcohol, potassium, calcium, magnesium, cholesterol." + )] + async fn set_goals( + &self, + Parameters(params): Parameters, + ) -> Result { + let pool = self.pool().await?; + + // Get existing goals + let row = sqlx::query("SELECT goals FROM daily_goal WHERE id = 1") + .fetch_one(&pool) + .await + .map_err(|e| McpError::internal_error(format!("Goals query failed: {e}"), None))?; + + let existing_str: String = row.try_get("goals").map_err(|e| McpError::internal_error(e.to_string(), None))?; + let mut existing: Value = serde_json::from_str(&existing_str).unwrap_or(json!({})); + + // Merge new goals into existing + if let (Some(existing_obj), Some(new_obj)) = (existing.as_object_mut(), params.goals.as_object()) { + for (k, v) in new_obj { + existing_obj.insert(k.clone(), v.clone()); + } + } + + // Save + sqlx::query("UPDATE daily_goal SET goals = ?, updated_at = datetime('now') WHERE id = 1") + .bind(existing.to_string()) + .execute(&pool) + .await + .map_err(|e| McpError::internal_error(format!("Goals update failed: {e}"), None))?; + + ok(format!("✓ Updated daily goals:\n{}", serde_json::to_string_pretty(&existing).unwrap_or_default())) + } + + // ── Weight ─────────────────────────────────────────────────── + + #[tool( + description = "Log body weight in kilograms for a date (default: today)." + )] + async fn log_weight( + &self, + Parameters(params): Parameters, + ) -> Result { + let pool = self.pool().await?; + let date = params.date.unwrap_or_else(today); + + sqlx::query( + r#"INSERT INTO weight_log (date, weight_kg) VALUES (?, ?) + ON CONFLICT DO UPDATE SET weight_kg = excluded.weight_kg, created_at = datetime('now')"#, + ) + .bind(&date) + .bind(params.weight_kg) + .execute(&pool) + .await + .map_err(|e| McpError::internal_error(format!("Weight insert failed: {e}"), None))?; + + ok(format!("✓ Logged weight: {} kg ({})", params.weight_kg, date)) + } + + #[tool( + description = "Show weight history for the last N days (default 30)." + )] + async fn weight_history( + &self, + Parameters(params): Parameters, + ) -> Result { + let pool = self.pool().await?; + let days = params.days.unwrap_or(30); + + let rows = sqlx::query( + r#"SELECT date, weight_kg FROM weight_log + WHERE date >= date('now', ?) + ORDER BY date DESC"#, + ) + .bind(format!("-{} days", days)) + .fetch_all(&pool) + .await + .map_err(|e| McpError::internal_error(format!("Weight history query failed: {e}"), None))?; + + if rows.is_empty() { + return ok(format!("No weight entries in the last {} days", days)); + } + + let mut entries = Vec::new(); + for row in rows { + let date: String = row.try_get("date").map_err(|e| McpError::internal_error(e.to_string(), None))?; + let weight_kg: f64 = row.try_get("weight_kg").map_err(|e| McpError::internal_error(e.to_string(), None))?; + entries.push(json!({"date": date, "weight_kg": weight_kg})); + } + + ok(serde_json::to_string_pretty(&json!({ + "days": days, + "weight_log": entries + })) + .unwrap_or_default()) + } + + // ── Future: Bulk Import ────────────────────────────────────── + + #[tool( + description = "Bulk import OpenFood Facts data from a Parquet file for offline use. \ + NOT YET IMPLEMENTED — stub for future offline mode." + )] + async fn bulk_import( + &self, + Parameters(_params): Parameters, + ) -> Result { + ok( + "Not yet implemented. Future: download HuggingFace Parquet export, \ + extract relevant columns (code, product_name, brands, nutriments, \ + serving_quantity, serving_size, image_url), bulk insert into products \ + table, rebuild FTS5 index." + .to_string(), + ) + } +} + +// ── Daily summary helper ──────────────────────────────────────────── + +impl NutritionServer { + async fn build_daily_summary(&self, pool: &sqlx::SqlitePool, date: &str) -> Result { + // Fetch entries + let rows = sqlx::query( + r#"SELECT id, meal, food_name, grams, nutriments FROM entries WHERE date = ? ORDER BY created_at"#, + ) + .bind(date) + .fetch_all(pool) + .await + .map_err(|e| McpError::internal_error(format!("Summary query failed: {e}"), None))?; + + let entry_count = rows.len(); + let mut entries_nutriments: Vec = Vec::new(); + let mut entry_list: Vec = Vec::new(); + + for row in &rows { + let id: i64 = row.try_get("id").map_err(|e| McpError::internal_error(e.to_string(), None))?; + let meal: String = row.try_get("meal").map_err(|e| McpError::internal_error(e.to_string(), None))?; + let food_name: String = row.try_get("food_name").map_err(|e| McpError::internal_error(e.to_string(), None))?; + let grams: Option = row.try_get("grams").unwrap_or(None); + let nutriments_str: String = row.try_get("nutriments").map_err(|e| McpError::internal_error(e.to_string(), None))?; + let n: Value = serde_json::from_str(&nutriments_str).unwrap_or(json!({})); + entries_nutriments.push(n.clone()); + entry_list.push(json!({"id": id, "meal": meal, "food_name": food_name, "grams": grams})); + } + + let totals = sum_nutriments(&entries_nutriments); + + // Get goals + let goals_row = sqlx::query("SELECT goals FROM daily_goal WHERE id = 1") + .fetch_one(pool) + .await + .map_err(|e| McpError::internal_error(format!("Goals query failed: {e}"), None))?; + let goals_str: String = goals_row.try_get("goals").map_err(|e| McpError::internal_error(e.to_string(), None))?; + let goals: Value = serde_json::from_str(&goals_str).unwrap_or(json!({})); + + // Calculate remaining for key nutrients + let remaining = |key: &str| -> Value { + let consumed = totals.get(key).and_then(|v| v.as_f64()).unwrap_or(0.0); + let goal = goals.get(key).and_then(|v| v.as_f64()).unwrap_or(0.0); + json!(goal - consumed) + }; + + Ok(serde_json::to_string_pretty(&json!({ + "date": date, + "entries": entry_count, + "totals": totals, + "goals": goals, + "remaining": { + "energy_kcal": remaining("energy-kcal"), + "protein": remaining("protein"), + "fructose": remaining("fructose"), + "salt": remaining("salt"), + "potassium": remaining("potassium"), + }, + "entry_list": entry_list + })) + .unwrap_or_default()) + } +} + +// ── ServerHandler impl ────────────────────────────────────────────── + +#[tool_handler] +impl ServerHandler for NutritionServer { + fn get_info(&self) -> ServerInfo { + ServerInfo { + server_info: Implementation { + name: "nutrition-mcp".into(), + version: "0.1.0".into(), + title: Some("Nutrition Tracker".into()), + icons: None, + website_url: None, + }, + capabilities: ServerCapabilities::builder() + .enable_tools() + .build(), + instructions: Some( + "Personal nutrition tracking MCP server. Tools: search_food (find food by name), \ + get_food_by_barcode (lookup by barcode), log_food (log OFF product with portions), \ + log_custom_food (log homemade/restaurant food), delete_entry, daily_summary, \ + history, list_entries, get_goals, set_goals, log_weight, weight_history, \ + bulk_import (stub). Tracks 14 nutrients including gout (fructose, alcohol) and \ + hypertension (salt, potassium, calcium, magnesium, cholesterol) markers." + .into(), + ), + ..Default::default() + } + } +} \ No newline at end of file