mcp: add reference_script_path arg to bypass MCP large-string transport bug

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.
This commit is contained in:
Kayos 2026-05-07 16:45:53 -07:00
parent 288e5815a0
commit 2d4c2163a9

View file

@ -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<Option<Vec<u8>>, 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<String>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
/// "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<String, String> {
@ -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,
};