From 2d4c2163a9f8eb2b1d2da2d0feba0d025dfb9bf1 Mon Sep 17 00:00:00 2001 From: Kayos Date: Thu, 7 May 2026 16:45:53 -0700 Subject: [PATCH] mcp: add reference_script_path arg to bypass MCP large-string transport bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caught 2026-05-07: hex strings passed to MCP tool args > ~4500 chars get a 1-byte truncation + structural rearrangement somewhere between Claude Code and aldabra's stdio reader. Pallas + aldabra-core's hex_decode are byte-clean (verified via crates/aldabra-dao/examples/ repro_script_corruption.rs); the corruption is purely in the JSON-RPC-over-stdio transport layer. Workaround: accept reference_script_path that points at a file inside the aldabra container. Caller docker-cp's the hex file in, passes the path via MCP arg, aldabra reads bytes locally — no large strings cross the JSON-RPC wire. Applies to wallet_send + wallet_send_unsigned. wallet_plutus_mint_* tools still ride the hex-string path (small policies only, < 4500 chars). When we hit a Plutus policy that needs the workaround, port the same pattern. --- crates/aldabra-mcp/src/tools.rs | 83 ++++++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/crates/aldabra-mcp/src/tools.rs b/crates/aldabra-mcp/src/tools.rs index cb06d94..4fd1800 100644 --- a/crates/aldabra-mcp/src/tools.rs +++ b/crates/aldabra-mcp/src/tools.rs @@ -59,6 +59,47 @@ use aldabra_core::{ ProtocolParams, ReferenceScriptSpec, ScriptKind, StakeKey, DEFAULT_EX_UNITS, }; +/// Resolve a reference-script bytestring from EITHER an inline hex +/// argument OR a file path inside the container. Caller passes both +/// raw options; this fn enforces the "at most one" rule and reads +/// the file when path is set. +/// +/// The path-based variant exists because of the 2026-05-07 MCP +/// transport bug: hex strings >~ 4500 chars get a 1-byte truncation +/// + structural rearrangement somewhere between Claude Code and +/// aldabra's stdio reader. Reading from a file inside the container +/// bypasses the JSON-RPC arg path entirely. +fn resolve_ref_script_bytes( + cbor_hex: Option<&str>, + path: Option<&str>, +) -> Result>, String> { + match (cbor_hex, path) { + (Some(_), Some(_)) => Err( + "set at most one of reference_script_cbor_hex / reference_script_path".into(), + ), + (Some(s), None) => { + let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect(); + Ok(Some(hex_decode(&cleaned).map_err(|e| { + format!("decode reference_script_cbor_hex: {e}") + })?)) + } + (None, Some(p)) => { + let raw = std::fs::read_to_string(p) + .map_err(|e| format!("read reference_script_path '{p}': {e}"))?; + let cleaned: String = raw.chars().filter(|c| !c.is_whitespace()).collect(); + if cleaned.is_empty() { + return Err(format!( + "reference_script_path '{p}' contained no hex characters" + )); + } + Ok(Some(hex_decode(&cleaned).map_err(|e| { + format!("decode reference_script_path '{p}' contents: {e}") + })?)) + } + (None, None) => Ok(None), + } +} + /// Parse a user-supplied script-kind string ("PlutusV1" / "PlutusV2" /// / "PlutusV3" / "Native") into the pallas `ScriptKind` enum used /// by the reference-script attachment helper. Case-insensitive, @@ -215,6 +256,17 @@ pub struct SendArgs { /// Requires `reference_script_kind` to also be set. #[serde(default)] pub reference_script_cbor_hex: Option, + /// Path INSIDE THE ALDABRA CONTAINER to a file containing the + /// hex-encoded reference-script CBOR. Use INSTEAD of + /// `reference_script_cbor_hex` for scripts >~ 4KB to bypass the + /// MCP large-string transport bug (caught 2026-05-07: hex strings + /// > ~4500 chars get a 1-byte truncation + structural rearrangement + /// somewhere between Claude Code and aldabra's stdio reader). + /// File contents may include leading/trailing whitespace; only + /// hex chars are decoded. At most one of `reference_script_cbor_hex` + /// or `reference_script_path` may be set. + #[serde(default)] + pub reference_script_path: Option, /// Plutus version of the reference-script: "PlutusV1", "PlutusV2", /// "PlutusV3", or "Native". Required when reference_script_cbor_hex /// is set; ignored otherwise. @@ -298,6 +350,11 @@ pub struct UnsignedSendArgs { /// [`SendArgs::reference_script_cbor_hex`]. #[serde(default)] pub reference_script_cbor_hex: Option, + /// Path INSIDE THE ALDABRA CONTAINER to a hex file. See + /// [`SendArgs::reference_script_path`] — same workaround for the + /// MCP large-string transport bug. + #[serde(default)] + pub reference_script_path: Option, /// "PlutusV1" | "PlutusV2" | "PlutusV3" | "Native". See /// [`SendArgs::reference_script_kind`]. #[serde(default)] @@ -673,6 +730,7 @@ impl WalletService { assets, datum_inline_cbor_hex, reference_script_cbor_hex, + reference_script_path, reference_script_kind, force, }: SendArgs, @@ -723,20 +781,20 @@ impl WalletService { Some(s) => Some(hex_decode(s).map_err(|e| format!("decode datum: {e}"))?), None => None, }; - let ref_script_bytes = match reference_script_cbor_hex.as_deref() { - Some(s) => Some(hex_decode(s).map_err(|e| format!("decode reference_script: {e}"))?), - None => None, - }; + let ref_script_bytes = resolve_ref_script_bytes( + reference_script_cbor_hex.as_deref(), + reference_script_path.as_deref(), + )?; let ref_script = match (ref_script_bytes.as_ref(), reference_script_kind.as_deref()) { (Some(bytes), Some(kind)) => Some(ReferenceScriptSpec { kind: parse_script_kind(kind)?, cbor: bytes.as_slice(), }), (Some(_), None) => { - return Err("reference_script_cbor_hex set without reference_script_kind".into()) + return Err("reference_script_cbor_hex/path set without reference_script_kind".into()) } (None, Some(_)) => { - return Err("reference_script_kind set without reference_script_cbor_hex".into()) + return Err("reference_script_kind set without reference_script_cbor_hex/path".into()) } (None, None) => None, }; @@ -793,6 +851,7 @@ impl WalletService { assets, datum_inline_cbor_hex, reference_script_cbor_hex, + reference_script_path, reference_script_kind, }: UnsignedSendArgs, ) -> Result { @@ -826,20 +885,20 @@ impl WalletService { Some(s) => Some(hex_decode(s).map_err(|e| format!("decode datum: {e}"))?), None => None, }; - let ref_script_bytes = match reference_script_cbor_hex.as_deref() { - Some(s) => Some(hex_decode(s).map_err(|e| format!("decode reference_script: {e}"))?), - None => None, - }; + let ref_script_bytes = resolve_ref_script_bytes( + reference_script_cbor_hex.as_deref(), + reference_script_path.as_deref(), + )?; let ref_script = match (ref_script_bytes.as_ref(), reference_script_kind.as_deref()) { (Some(bytes), Some(kind)) => Some(ReferenceScriptSpec { kind: parse_script_kind(kind)?, cbor: bytes.as_slice(), }), (Some(_), None) => { - return Err("reference_script_cbor_hex set without reference_script_kind".into()) + return Err("reference_script_cbor_hex/path set without reference_script_kind".into()) } (None, Some(_)) => { - return Err("reference_script_kind set without reference_script_cbor_hex".into()) + return Err("reference_script_kind set without reference_script_cbor_hex/path".into()) } (None, None) => None, };