fix(mcp): force type:object on Value-typed metadata/policy args
schemars derives an empty (no-type) JSON Schema for Option<serde_json::Value>
and serde_json::Value fields. Claude Code's MCP client interprets schema-
without-type as 'string-encoded' and JSON-stringifies the user's {...}
before sending — at which point the server-side validation
`value.is_object()` returns false and the tool errors with
'CIP-25 metadata must be a JSON object' / 'CIP-68 metadata must be a
JSON object'.
Surfaced during Track #37 E2E test (2026-05-09): wallet_mint and
wallet_mint_cip68_nft both rejected metadata-as-object args from
Claude Code while the SAME object via raw stdio MCP works fine —
proves the issue is client-side schema interpretation, not server.
Fix: add a json_object_schema helper that emits {type: object,
additionalProperties: true} and annotate every metadata + policy
field with #[schemars(schema_with = "json_object_schema")].
Affected fields:
- MintArgs.metadata
- MintUnsignedArgs.policy + .metadata
- Cip68NftArgs.metadata
additionalProperties is left wide-open since these args really do
accept arbitrary keys (CIP-25/CIP-68 are open-ended schemas).
This commit is contained in:
parent
72bb8fa38e
commit
91c5d557b6
1 changed files with 27 additions and 0 deletions
|
|
@ -167,6 +167,29 @@ use rmcp::{
|
|||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Schema-shape helper for `serde_json::Value` arg fields that
|
||||
/// expect a JSON object (CIP-25 / CIP-68 metadata, multisig
|
||||
/// PolicySpec dicts). schemars's default for `Option<Value>` /
|
||||
/// `Value` emits a schema with no `type`, which Claude Code's MCP
|
||||
/// client interprets as "string-encoded" — it then JSON-stringifies
|
||||
/// the user's `{...}` before sending, and the server-side
|
||||
/// validation `value.is_object()` returns false. Force
|
||||
/// `type: object` so the client passes the value through as a
|
||||
/// proper JSON object on the wire. (`additionalProperties: true`
|
||||
/// keeps the schema permissive — these args really do accept
|
||||
/// arbitrary keys.)
|
||||
fn json_object_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
use schemars::schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec};
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
|
||||
object: Some(Box::new(ObjectValidation {
|
||||
additional_properties: Some(Box::new(Schema::Bool(true))),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// MCP-facing asset spec — separate from `aldabra_core::AssetSpec`
|
||||
/// so the JsonSchema derive doesn't bleed schemars into the
|
||||
/// security-boundary crate.
|
||||
|
|
@ -518,9 +541,11 @@ pub struct MintUnsignedArgs {
|
|||
/// single-sig policy bound to this wallet's payment key (same as
|
||||
/// `wallet_mint`).
|
||||
#[serde(default)]
|
||||
#[schemars(schema_with = "json_object_schema")]
|
||||
pub policy: Option<serde_json::Value>,
|
||||
/// Optional CIP-25 v2 metadata.
|
||||
#[serde(default)]
|
||||
#[schemars(schema_with = "json_object_schema")]
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
/// Hex of the pkh to disclose as a required signer in the tx
|
||||
/// body. Defaults to this wallet's payment key hash. For
|
||||
|
|
@ -665,6 +690,7 @@ pub struct Cip68NftArgs {
|
|||
/// CIP-68 metadata JSON object (`name`, `image`, `description`,
|
||||
/// `mediaType`, `files`, etc.). Encoded as Plutus Data and
|
||||
/// attached as the inline datum on the ref-NFT output.
|
||||
#[schemars(schema_with = "json_object_schema")]
|
||||
pub metadata: serde_json::Value,
|
||||
/// Optional address where the reference NFT lives. Defaults to
|
||||
/// the wallet's own address — keeps the NFT *mutable* (the
|
||||
|
|
@ -722,6 +748,7 @@ pub struct MintArgs {
|
|||
/// `files`, etc.). Wallets and explorers display this when
|
||||
/// rendering the asset.
|
||||
#[serde(default)]
|
||||
#[schemars(schema_with = "json_object_schema")]
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
/// Bypass the configured `max_send_lovelace` hard cap on
|
||||
/// `dest_lovelace`. Only pass `true` for an intentional,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue