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:
parent
288e5815a0
commit
2d4c2163a9
1 changed files with 71 additions and 12 deletions
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue