From 7b6e8c3ed0e6ed99a8212a438c26982cf42a84e8 Mon Sep 17 00:00:00 2001 From: Alessandro Siniscalchi Date: Mon, 18 May 2026 06:39:14 +0000 Subject: [PATCH 1/4] Run MCP Streamable HTTP server in stateless mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server used rmcp's default stateful_mode, keeping every session in LocalSessionManager's RAM. Any restart of the backend invalidated existing session IDs; clients that did not renegotiate initialize — observed with Gemini CLI — kept reusing the stale ID and received 404 Not Found indefinitely. PortfolioServer only exposes request/response tools and does not rely on server-initiated SSE streams, so stateless mode covers the full surface while making each request self-contained and immune to session loss across restarts. --- backend/src/mcp_server.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/mcp_server.rs b/backend/src/mcp_server.rs index 24b634e..e6d09de 100644 --- a/backend/src/mcp_server.rs +++ b/backend/src/mcp_server.rs @@ -710,7 +710,9 @@ pub fn build_mcp_service( StreamableHttpService::new( move || Ok(PortfolioServer::new(pool.clone())), LocalSessionManager::default().into(), - StreamableHttpServerConfig::default().disable_allowed_hosts(), + StreamableHttpServerConfig::default() + .disable_allowed_hosts() + .with_stateful_mode(false), ) } From 420d74b8758c0a3aa4dae9a0a86f376aea6a1cc7 Mon Sep 17 00:00:00 2001 From: Alessandro Siniscalchi Date: Mon, 25 May 2026 13:28:48 +0000 Subject: [PATCH 2/4] fix(backend): configure reqwest Client with browser User-Agent to prevent 403 Forbidden on crypto price APIs --- backend/src/asset_price_refresh/mod.rs | 3 +-- backend/src/fx_refresh.rs | 2 +- backend/src/graphql/mod.rs | 2 +- backend/src/lib.rs | 8 ++++++++ backend/src/main.rs | 6 +++--- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/backend/src/asset_price_refresh/mod.rs b/backend/src/asset_price_refresh/mod.rs index 71febd3..90da866 100644 --- a/backend/src/asset_price_refresh/mod.rs +++ b/backend/src/asset_price_refresh/mod.rs @@ -15,14 +15,13 @@ pub use providers::{ pub use refresh::{fill_missing_asset_prices, refresh_asset_prices, refresh_single_asset_price}; pub use types::{AssetPriceRefreshError, AssetQuote}; -use reqwest::Client; use sqlx::SqlitePool; use tokio::time::sleep; use tracing::{info, warn}; pub async fn spawn_asset_price_refresh_task(pool: SqlitePool, config: AssetPriceRefreshConfig) { tokio::spawn(async move { - let client = Client::new(); + let client = crate::new_http_client(); info!( refresh_interval_seconds = config.refresh_interval.as_secs(), diff --git a/backend/src/fx_refresh.rs b/backend/src/fx_refresh.rs index 4a62075..c2d89a4 100644 --- a/backend/src/fx_refresh.rs +++ b/backend/src/fx_refresh.rs @@ -87,7 +87,7 @@ pub async fn spawn_fx_refresh_task( config: FxRefreshConfig, ) { tokio::spawn(async move { - let client = Client::new(); + let client = crate::new_http_client(); info!( refresh_interval_seconds = config.refresh_interval.as_secs(), diff --git a/backend/src/graphql/mod.rs b/backend/src/graphql/mod.rs index 6b502e8..5695c62 100644 --- a/backend/src/graphql/mod.rs +++ b/backend/src/graphql/mod.rs @@ -62,7 +62,7 @@ pub fn build_router(pool: SqlitePool) -> Router { pool, fx_refresh_status: crate::new_shared_fx_refresh_status(), asset_price_refresh_config: config.asset_price_refresh_config(), - http_client: reqwest::Client::new(), + http_client: crate::new_http_client(), config_markdown: config.to_markdown(), web_dir: None, }) diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 451bee1..e5b92bf 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -90,3 +90,11 @@ pub use storage::{ update_asset_transaction, update_todo_completed, upsert_asset_price, upsert_asset_quote_source, upsert_fx_rate, }; + +pub fn new_http_client() -> reqwest::Client { + reqwest::Client::builder() + .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") + .build() + .unwrap_or_else(|_| reqwest::Client::new()) +} + diff --git a/backend/src/main.rs b/backend/src/main.rs index 6e42aca..b66c46b 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -6,8 +6,8 @@ use tracing::{error, info, warn}; use backend::{ AppState, AssetPriceRefreshConfig, Config, FxRefreshConfig, build_router_with_state, - connect_db_file, init_tracing, new_shared_fx_refresh_status, spawn_asset_price_refresh_task, - spawn_fx_refresh_task, spawn_portfolio_snapshot_task, + connect_db_file, init_tracing, new_http_client, new_shared_fx_refresh_status, + spawn_asset_price_refresh_task, spawn_fx_refresh_task, spawn_portfolio_snapshot_task, }; #[tokio::main] @@ -36,7 +36,7 @@ async fn main() { let fx_refresh_status = new_shared_fx_refresh_status(); let fx_refresh_config = config.fx_refresh_config(); let asset_price_refresh_config = config.asset_price_refresh_config(); - let http_client = reqwest::Client::new(); + let http_client = new_http_client(); let web_dir = resolve_web_dir(&config.web_dir); let app = build_router_with_state(AppState { From 69718c2bc9633e381b9fcafc30e803bdcc325698 Mon Sep 17 00:00:00 2001 From: Alessandro Siniscalchi Date: Mon, 25 May 2026 17:49:44 +0000 Subject: [PATCH 3/4] chore(backend): run cargo fmt to resolve formatting check --- backend/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/lib.rs b/backend/src/lib.rs index e5b92bf..57fe23e 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -97,4 +97,3 @@ pub fn new_http_client() -> reqwest::Client { .build() .unwrap_or_else(|_| reqwest::Client::new()) } - From 93767fc1276a9aa87165576c9fe166a431785a0d Mon Sep 17 00:00:00 2001 From: Alessandro Siniscalchi Date: Mon, 25 May 2026 17:50:13 +0000 Subject: [PATCH 4/4] chore(web): run npm audit fix to resolve security vulnerabilities --- web/package-lock.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 950b14a..d85dbab 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -7228,9 +7228,9 @@ } }, "node_modules/@ts-morph/common/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -7670,9 +7670,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -10464,9 +10464,9 @@ } }, "node_modules/graphql-config/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -13540,9 +13540,9 @@ } }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -15945,9 +15945,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "devOptional": true, "license": "MIT", "engines": {