@sundaeswap/sprinkles 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +178 -181
  2. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js +4 -4
  3. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js.map +1 -1
  4. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js +25 -3
  5. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js.map +1 -1
  6. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
  7. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  8. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js +7 -9
  9. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -1
  10. package/dist/cjs/Sprinkle/__tests__/native-script.test.js +390 -0
  11. package/dist/cjs/Sprinkle/__tests__/native-script.test.js.map +1 -0
  12. package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js +367 -0
  13. package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
  14. package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js +164 -0
  15. package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
  16. package/dist/cjs/Sprinkle/actions/builtin/index.js +60 -3
  17. package/dist/cjs/Sprinkle/actions/builtin/index.js.map +1 -1
  18. package/dist/cjs/Sprinkle/actions/builtin/native-script.js +139 -0
  19. package/dist/cjs/Sprinkle/actions/builtin/native-script.js.map +1 -0
  20. package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js +218 -0
  21. package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
  22. package/dist/cjs/Sprinkle/actions/cli-adapter.js +20 -2
  23. package/dist/cjs/Sprinkle/actions/cli-adapter.js.map +1 -1
  24. package/dist/cjs/Sprinkle/actions/index.js +12 -0
  25. package/dist/cjs/Sprinkle/actions/index.js.map +1 -1
  26. package/dist/cjs/Sprinkle/actions/mcp-adapter.js +146 -4
  27. package/dist/cjs/Sprinkle/actions/mcp-adapter.js.map +1 -1
  28. package/dist/cjs/Sprinkle/index.js +267 -5
  29. package/dist/cjs/Sprinkle/index.js.map +1 -1
  30. package/dist/cjs/Sprinkle/schemas.js +17 -1
  31. package/dist/cjs/Sprinkle/schemas.js.map +1 -1
  32. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js +4 -4
  33. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js.map +1 -1
  34. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js +25 -3
  35. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js.map +1 -1
  36. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
  37. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  38. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js +7 -9
  39. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -1
  40. package/dist/esm/Sprinkle/__tests__/native-script.test.js +388 -0
  41. package/dist/esm/Sprinkle/__tests__/native-script.test.js.map +1 -0
  42. package/dist/esm/Sprinkle/__tests__/utility-actions.test.js +365 -0
  43. package/dist/esm/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
  44. package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js +159 -0
  45. package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
  46. package/dist/esm/Sprinkle/actions/builtin/index.js +8 -3
  47. package/dist/esm/Sprinkle/actions/builtin/index.js.map +1 -1
  48. package/dist/esm/Sprinkle/actions/builtin/native-script.js +133 -0
  49. package/dist/esm/Sprinkle/actions/builtin/native-script.js.map +1 -0
  50. package/dist/esm/Sprinkle/actions/builtin/utility-actions.js +213 -0
  51. package/dist/esm/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
  52. package/dist/esm/Sprinkle/actions/cli-adapter.js +20 -2
  53. package/dist/esm/Sprinkle/actions/cli-adapter.js.map +1 -1
  54. package/dist/esm/Sprinkle/actions/index.js +1 -1
  55. package/dist/esm/Sprinkle/actions/index.js.map +1 -1
  56. package/dist/esm/Sprinkle/actions/mcp-adapter.js +145 -5
  57. package/dist/esm/Sprinkle/actions/mcp-adapter.js.map +1 -1
  58. package/dist/esm/Sprinkle/index.js +259 -8
  59. package/dist/esm/Sprinkle/index.js.map +1 -1
  60. package/dist/esm/Sprinkle/schemas.js +16 -0
  61. package/dist/esm/Sprinkle/schemas.js.map +1 -1
  62. package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts +50 -0
  63. package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts.map +1 -0
  64. package/dist/types/Sprinkle/actions/builtin/index.d.ts +6 -2
  65. package/dist/types/Sprinkle/actions/builtin/index.d.ts.map +1 -1
  66. package/dist/types/Sprinkle/actions/builtin/native-script.d.ts +27 -0
  67. package/dist/types/Sprinkle/actions/builtin/native-script.d.ts.map +1 -0
  68. package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts +48 -0
  69. package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts.map +1 -0
  70. package/dist/types/Sprinkle/actions/cli-adapter.d.ts.map +1 -1
  71. package/dist/types/Sprinkle/actions/index.d.ts +2 -1
  72. package/dist/types/Sprinkle/actions/index.d.ts.map +1 -1
  73. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts +24 -0
  74. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts.map +1 -1
  75. package/dist/types/Sprinkle/index.d.ts +3 -1
  76. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  77. package/dist/types/Sprinkle/schemas.d.ts +72 -0
  78. package/dist/types/Sprinkle/schemas.d.ts.map +1 -1
  79. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  80. package/package.json +1 -1
  81. package/src/Sprinkle/__tests__/builtin-actions.test.ts +4 -4
  82. package/src/Sprinkle/__tests__/cli-adapter.test.ts +24 -3
  83. package/src/Sprinkle/__tests__/fill-in-struct.test.ts +23 -1
  84. package/src/Sprinkle/__tests__/mcp-adapter.test.ts +7 -5
  85. package/src/Sprinkle/__tests__/native-script.test.ts +341 -0
  86. package/src/Sprinkle/__tests__/utility-actions.test.ts +348 -0
  87. package/src/Sprinkle/actions/builtin/addressbook-actions.ts +168 -0
  88. package/src/Sprinkle/actions/builtin/index.ts +41 -2
  89. package/src/Sprinkle/actions/builtin/native-script.ts +165 -0
  90. package/src/Sprinkle/actions/builtin/utility-actions.ts +285 -0
  91. package/src/Sprinkle/actions/cli-adapter.ts +18 -2
  92. package/src/Sprinkle/actions/index.ts +2 -1
  93. package/src/Sprinkle/actions/mcp-adapter.ts +179 -4
  94. package/src/Sprinkle/index.ts +261 -3
  95. package/src/Sprinkle/schemas.ts +20 -0
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Native script utilities for Sprinkle actions.
3
+ *
4
+ * Provides conversion from user-friendly input formats (CBOR hex or structured JSON)
5
+ * to Blaze NativeScript objects, and a helper to attach scripts during tx completion.
6
+ */
7
+
8
+ import { Core } from "@blaze-cardano/sdk";
9
+ import type { TxBuilder } from "@blaze-cardano/tx";
10
+ import type { TSchema } from "@sinclair/typebox";
11
+ import type { TMultisigScript } from "../../schemas.js";
12
+ import { ActionError } from "../types.js";
13
+ import type { IActionContext } from "../types.js";
14
+
15
+ /** Input type: either a CBOR hex string or a MultisigScript JSON structure */
16
+ export type NativeScriptInput = string | TMultisigScript;
17
+
18
+ /**
19
+ * Converts a MultisigScript JSON structure to a Blaze NativeScript.
20
+ * Handles the recursive structure (AllOf, AnyOf, AtLeast contain nested scripts).
21
+ */
22
+ function multisigToNativeScript(ms: TMultisigScript): Core.NativeScript {
23
+ if ("Signature" in ms) {
24
+ const hash = Core.Ed25519KeyHashHex(ms.Signature.key_hash);
25
+ return Core.NativeScript.newScriptPubkey(new Core.ScriptPubkey(hash));
26
+ }
27
+ if ("AllOf" in ms) {
28
+ const scripts = ms.AllOf.scripts.map(multisigToNativeScript);
29
+ return Core.NativeScript.newScriptAll(new Core.ScriptAll(scripts));
30
+ }
31
+ if ("AnyOf" in ms) {
32
+ const scripts = ms.AnyOf.scripts.map(multisigToNativeScript);
33
+ return Core.NativeScript.newScriptAny(new Core.ScriptAny(scripts));
34
+ }
35
+ if ("AtLeast" in ms) {
36
+ const scripts = ms.AtLeast.scripts.map(multisigToNativeScript);
37
+ return Core.NativeScript.newScriptNOfK(
38
+ new Core.ScriptNOfK(scripts, Number(ms.AtLeast.required)),
39
+ );
40
+ }
41
+ if ("Before" in ms) {
42
+ return Core.NativeScript.newTimelockExpiry(
43
+ new Core.TimelockExpiry(Core.Slot(Number(ms.Before.time))),
44
+ );
45
+ }
46
+ if ("After" in ms) {
47
+ return Core.NativeScript.newTimelockStart(
48
+ new Core.TimelockStart(Core.Slot(Number(ms.After.time))),
49
+ );
50
+ }
51
+ if ("Script" in ms) {
52
+ // Script variant references a script by hash — this is a ScriptPubkey
53
+ // but using the script hash directly as a key hash reference
54
+ const hash = Core.Ed25519KeyHashHex(ms.Script.script_hash);
55
+ return Core.NativeScript.newScriptPubkey(new Core.ScriptPubkey(hash));
56
+ }
57
+
58
+ throw new ActionError(
59
+ "Unrecognized MultisigScript variant",
60
+ "INVALID_NATIVE_SCRIPT",
61
+ { input: ms },
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Converts a native script input (CBOR hex string or MultisigScript JSON)
67
+ * to a Blaze Script object suitable for `provideScript()`.
68
+ */
69
+ export function toNativeScript(input: NativeScriptInput): Core.Script {
70
+ if (typeof input === "string") {
71
+ try {
72
+ const nativeScript = Core.NativeScript.fromCbor(
73
+ Core.HexBlob(input),
74
+ );
75
+ return Core.Script.newNativeScript(nativeScript);
76
+ } catch (err) {
77
+ throw new ActionError(
78
+ `Invalid native script CBOR: ${err instanceof Error ? err.message : String(err)}`,
79
+ "INVALID_NATIVE_SCRIPT",
80
+ { error: err instanceof Error ? err.message : String(err) },
81
+ );
82
+ }
83
+ }
84
+
85
+ try {
86
+ const nativeScript = multisigToNativeScript(input);
87
+ return Core.Script.newNativeScript(nativeScript);
88
+ } catch (err) {
89
+ if (err instanceof ActionError) throw err;
90
+ throw new ActionError(
91
+ `Failed to convert MultisigScript to native script: ${err instanceof Error ? err.message : String(err)}`,
92
+ "INVALID_NATIVE_SCRIPT",
93
+ { error: err instanceof Error ? err.message : String(err) },
94
+ );
95
+ }
96
+ }
97
+
98
+ /** Regex to extract a script hash from Blaze's "Could not resolve script hash" error */
99
+ const MISSING_SCRIPT_HASH_RE =
100
+ /complete: Could not resolve script hash[: ]*([0-9a-fA-F]+)/;
101
+
102
+ /**
103
+ * Builds a hash → Script lookup from the addressbook in the sprinkle context.
104
+ * Returns an empty map if there's no addressbook.
105
+ */
106
+ function buildAddressbookIndex(
107
+ addressbook: Record<string, TMultisigScript> | undefined,
108
+ ): Map<string, Core.Script> {
109
+ const index = new Map<string, Core.Script>();
110
+ if (!addressbook) return index;
111
+ for (const [, ms] of Object.entries(addressbook)) {
112
+ try {
113
+ const script = toNativeScript(ms);
114
+ index.set(script.hash(), script);
115
+ } catch {
116
+ // Skip invalid addressbook entries
117
+ }
118
+ }
119
+ return index;
120
+ }
121
+
122
+ /**
123
+ * Attaches optional native scripts to a transaction builder and completes it.
124
+ *
125
+ * If completion fails because Blaze cannot resolve a required native script hash,
126
+ * and a context with an addressbook is provided, it will look up the missing script
127
+ * by hash in the addressbook and retry once.
128
+ */
129
+ export async function completeWithScripts<S extends TSchema>(
130
+ txBuilder: TxBuilder,
131
+ nativeScripts?: NativeScriptInput[],
132
+ context?: IActionContext<S>,
133
+ ): Promise<Core.Transaction> {
134
+ if (nativeScripts?.length) {
135
+ for (const input of nativeScripts) {
136
+ txBuilder.provideScript(toNativeScript(input));
137
+ }
138
+ }
139
+
140
+ try {
141
+ return await txBuilder.complete();
142
+ } catch (err) {
143
+ const message = err instanceof Error ? err.message : String(err);
144
+ const match = message.match(MISSING_SCRIPT_HASH_RE);
145
+
146
+ if (!match || !context?.sprinkle?.addressbook) {
147
+ throw err;
148
+ }
149
+
150
+ // Build hash index from addressbook and try to resolve the missing script
151
+ const addressbookIndex = buildAddressbookIndex(
152
+ context.sprinkle.addressbook,
153
+ );
154
+ const missingHash = match[1];
155
+ const script = addressbookIndex.get(missingHash);
156
+
157
+ if (!script) {
158
+ throw err;
159
+ }
160
+
161
+ // Attach the found script and retry
162
+ txBuilder.provideScript(script);
163
+ return txBuilder.complete();
164
+ }
165
+ }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Built-in utility actions for the Sprinkle action system.
3
+ * These actions provide common Cardano operations: minting tokens,
4
+ * sending ADA/tokens, and registering stake scripts.
5
+ */
6
+
7
+ import { Type } from "@sinclair/typebox";
8
+ import type { TSchema } from "@sinclair/typebox";
9
+ import { Core, makeValue } from "@blaze-cardano/sdk";
10
+ import { ActionError } from "../types.js";
11
+ import type { IAction } from "../types.js";
12
+ import { getBlazeFromContext, isHotWallet } from "./blaze-helper.js";
13
+ import { NativeScriptsParam } from "../../schemas.js";
14
+ import type { NativeScriptInput } from "./native-script.js";
15
+ import { completeWithScripts } from "./native-script.js";
16
+
17
+ /**
18
+ * `mint-token` -- Mint native tokens using a native script derived from the wallet's payment key.
19
+ * The policy is a simple ScriptPubkey requiring the wallet's payment credential to sign.
20
+ * Requires a hot wallet.
21
+ */
22
+ export const mintToken: IAction<
23
+ { tokenName: string; amount: string; nativeScripts?: NativeScriptInput[] },
24
+ { policyId: string; tokenName: string; amount: string; txCbor: string },
25
+ TSchema
26
+ > = {
27
+ name: "mint-token",
28
+ description:
29
+ "Mint native tokens using a native script policy derived from the wallet's payment key.",
30
+ category: "utility",
31
+ inputSchema: Type.Object({
32
+ tokenName: Type.String({ description: "Name of the token to mint" }),
33
+ amount: Type.String({
34
+ description: "Amount of tokens to mint (as string for BigInt safety)",
35
+ }),
36
+ nativeScripts: NativeScriptsParam,
37
+ }),
38
+ outputSchema: Type.Object({
39
+ policyId: Type.String({ description: "Policy ID of the minted token" }),
40
+ tokenName: Type.String({ description: "Token name" }),
41
+ amount: Type.String({ description: "Amount minted" }),
42
+ txCbor: Type.String({
43
+ description: "Unsigned transaction CBOR hex ready for signing",
44
+ }),
45
+ }),
46
+ execute: async (input, context) => {
47
+ const blaze = await getBlazeFromContext(context);
48
+
49
+ if (!isHotWallet(blaze)) {
50
+ throw new ActionError(
51
+ "Mint token requires a hot wallet to derive the native script policy.",
52
+ "COLD_WALLET",
53
+ );
54
+ }
55
+
56
+ // Get the wallet's payment credential hash for the native script
57
+ let address: Core.Address;
58
+ try {
59
+ address = await blaze.wallet.getChangeAddress();
60
+ } catch (err) {
61
+ throw new ActionError(
62
+ `Failed to get wallet address: ${err instanceof Error ? err.message : String(err)}`,
63
+ "NO_ADDRESS",
64
+ { error: err instanceof Error ? err.message : String(err) },
65
+ );
66
+ }
67
+
68
+ const baseAddress = address.asBase();
69
+ if (!baseAddress) {
70
+ throw new ActionError(
71
+ "Could not derive base address from wallet. Ensure the wallet has a payment credential.",
72
+ "INVALID_ADDRESS",
73
+ );
74
+ }
75
+
76
+ const paymentCredential = baseAddress.getPaymentCredential();
77
+ if (!paymentCredential?.hash) {
78
+ throw new ActionError(
79
+ "Could not extract payment credential hash from wallet address.",
80
+ "NO_PAYMENT_CREDENTIAL",
81
+ );
82
+ }
83
+
84
+ const hash = Core.Ed25519KeyHashHex(paymentCredential.hash.toString());
85
+ const tokenPolicy = new Core.ScriptPubkey(hash);
86
+ const policy = Core.Script.newNativeScript(
87
+ Core.NativeScript.newScriptPubkey(tokenPolicy),
88
+ );
89
+
90
+ const policyId = policy.hash();
91
+ const assetName = Core.AssetName(
92
+ Core.toHex(Buffer.from(input.tokenName)),
93
+ );
94
+ const mintAmount = BigInt(input.amount);
95
+
96
+ const mints = new Map<Core.AssetName, bigint>();
97
+ mints.set(assetName, mintAmount);
98
+
99
+ let tx: Core.Transaction;
100
+ try {
101
+ const txBuilder = blaze
102
+ .newTransaction()
103
+ .addMint(Core.PolicyId(policyId), mints)
104
+ .provideScript(policy);
105
+ tx = await completeWithScripts(txBuilder, input.nativeScripts, context);
106
+ } catch (err) {
107
+ if (err instanceof ActionError) throw err;
108
+ throw new ActionError(
109
+ `Failed to build mint transaction: ${err instanceof Error ? err.message : String(err)}`,
110
+ "BUILD_ERROR",
111
+ { error: err instanceof Error ? err.message : String(err) },
112
+ );
113
+ }
114
+
115
+ return {
116
+ policyId: policyId.toString(),
117
+ tokenName: input.tokenName,
118
+ amount: input.amount,
119
+ txCbor: tx.toCbor(),
120
+ };
121
+ },
122
+ };
123
+
124
+ /**
125
+ * `simple-send` -- Send ADA or a native token to a recipient address.
126
+ * Builds an unsigned transaction that can be signed via TxDialog or sign-transaction.
127
+ */
128
+ export const simpleSend: IAction<
129
+ {
130
+ recipientAddress: string;
131
+ lovelace?: string;
132
+ policyId?: string;
133
+ assetName?: string;
134
+ tokenAmount?: string;
135
+ nativeScripts?: NativeScriptInput[];
136
+ },
137
+ { txCbor: string },
138
+ TSchema
139
+ > = {
140
+ name: "simple-send",
141
+ description: "Send ADA or a native token to a recipient address.",
142
+ category: "wallet",
143
+ inputSchema: Type.Object({
144
+ recipientAddress: Type.String({
145
+ description: "Bech32 recipient address",
146
+ }),
147
+ lovelace: Type.Optional(
148
+ Type.String({
149
+ description:
150
+ "Amount of lovelace to send (as string for BigInt safety). Required if not sending a token.",
151
+ }),
152
+ ),
153
+ policyId: Type.Optional(
154
+ Type.String({ description: "Policy ID of the token to send" }),
155
+ ),
156
+ assetName: Type.Optional(
157
+ Type.String({
158
+ description: "Asset name of the token to send (hex-encoded)",
159
+ }),
160
+ ),
161
+ tokenAmount: Type.Optional(
162
+ Type.String({
163
+ description: "Amount of tokens to send (as string for BigInt safety)",
164
+ }),
165
+ ),
166
+ nativeScripts: NativeScriptsParam,
167
+ }),
168
+ outputSchema: Type.Object({
169
+ txCbor: Type.String({
170
+ description: "Unsigned transaction CBOR hex ready for signing",
171
+ }),
172
+ }),
173
+ execute: async (input, context) => {
174
+ const blaze = await getBlazeFromContext(context);
175
+
176
+ // Validate: must provide lovelace or token details
177
+ const hasToken = input.policyId && input.assetName && input.tokenAmount;
178
+ if (!input.lovelace && !hasToken) {
179
+ throw new ActionError(
180
+ "Must provide either lovelace amount or token details (policyId, assetName, tokenAmount).",
181
+ "INVALID_INPUT",
182
+ );
183
+ }
184
+
185
+ let recipient: Core.Address;
186
+ try {
187
+ recipient = Core.Address.fromBech32(input.recipientAddress);
188
+ } catch (err) {
189
+ throw new ActionError(
190
+ `Invalid recipient address: ${err instanceof Error ? err.message : String(err)}`,
191
+ "INVALID_ADDRESS",
192
+ { error: err instanceof Error ? err.message : String(err) },
193
+ );
194
+ }
195
+
196
+ let value: Core.Value;
197
+ if (hasToken) {
198
+ const assetId =
199
+ `${input.policyId}${input.assetName}` as Core.AssetId;
200
+ value = makeValue(
201
+ BigInt(input.lovelace ?? "0"),
202
+ [assetId, BigInt(input.tokenAmount!)],
203
+ );
204
+ } else {
205
+ value = makeValue(BigInt(input.lovelace!));
206
+ }
207
+
208
+ let tx: Core.Transaction;
209
+ try {
210
+ const txBuilder = blaze.newTransaction().payAssets(recipient, value);
211
+ tx = await completeWithScripts(txBuilder, input.nativeScripts, context);
212
+ } catch (err) {
213
+ if (err instanceof ActionError) throw err;
214
+ throw new ActionError(
215
+ `Failed to build send transaction: ${err instanceof Error ? err.message : String(err)}`,
216
+ "BUILD_ERROR",
217
+ { error: err instanceof Error ? err.message : String(err) },
218
+ );
219
+ }
220
+
221
+ return {
222
+ txCbor: tx.toCbor(),
223
+ };
224
+ },
225
+ };
226
+
227
+ /**
228
+ * `register-stake-script` -- Register a stake script credential on-chain.
229
+ * Takes a script hash and builds a stake registration transaction.
230
+ */
231
+ export const registerStakeScript: IAction<
232
+ { scriptHash: string; nativeScripts?: NativeScriptInput[] },
233
+ { txCbor: string },
234
+ TSchema
235
+ > = {
236
+ name: "register-stake-script",
237
+ description:
238
+ "Register a stake script credential on-chain. Takes a script hash (hex) and builds the registration transaction.",
239
+ category: "utility",
240
+ inputSchema: Type.Object({
241
+ scriptHash: Type.String({
242
+ description: "Stake script hash in hex (28 bytes / 56 hex characters)",
243
+ }),
244
+ nativeScripts: NativeScriptsParam,
245
+ }),
246
+ outputSchema: Type.Object({
247
+ txCbor: Type.String({
248
+ description: "Unsigned transaction CBOR hex ready for signing",
249
+ }),
250
+ }),
251
+ execute: async (input, context) => {
252
+ const blaze = await getBlazeFromContext(context);
253
+
254
+ // Validate hex format
255
+ if (!/^[0-9a-fA-F]{56}$/.test(input.scriptHash)) {
256
+ throw new ActionError(
257
+ "Script hash must be exactly 56 hex characters (28 bytes).",
258
+ "INVALID_SCRIPT_HASH",
259
+ { provided: input.scriptHash },
260
+ );
261
+ }
262
+
263
+ const credential = Core.Credential.fromCore({
264
+ hash: Core.Hash28ByteBase16(input.scriptHash),
265
+ type: Core.CredentialType.ScriptHash,
266
+ });
267
+
268
+ let tx: Core.Transaction;
269
+ try {
270
+ const txBuilder = blaze.newTransaction().addRegisterStake(credential);
271
+ tx = await completeWithScripts(txBuilder, input.nativeScripts, context);
272
+ } catch (err) {
273
+ if (err instanceof ActionError) throw err;
274
+ throw new ActionError(
275
+ `Failed to build stake registration transaction: ${err instanceof Error ? err.message : String(err)}`,
276
+ "BUILD_ERROR",
277
+ { error: err instanceof Error ? err.message : String(err) },
278
+ );
279
+ }
280
+
281
+ return {
282
+ txCbor: tx.toCbor(),
283
+ };
284
+ },
285
+ };
@@ -132,18 +132,34 @@ export function coerceValue(raw: unknown, schema: TSchema): unknown {
132
132
  }
133
133
 
134
134
  if (isObject(schema)) {
135
+ let obj = raw;
135
136
  // If it's a JSON string, parse it
136
137
  if (typeof raw === "string") {
137
138
  const trimmed = raw.trim();
138
139
  if (trimmed.startsWith("{")) {
139
140
  try {
140
- return JSON.parse(trimmed);
141
+ obj = JSON.parse(trimmed);
141
142
  } catch {
142
143
  return raw;
143
144
  }
145
+ } else {
146
+ return raw;
144
147
  }
145
148
  }
146
- return raw;
149
+ // Recurse into properties to coerce nested values
150
+ if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) {
151
+ const props = schema.properties as Record<string, TSchema> | undefined;
152
+ if (props) {
153
+ const result: Record<string, unknown> = { ...(obj as Record<string, unknown>) };
154
+ for (const [key, propSchema] of Object.entries(props)) {
155
+ if (key in result) {
156
+ result[key] = coerceValue(result[key], propSchema);
157
+ }
158
+ }
159
+ return result;
160
+ }
161
+ }
162
+ return obj;
147
163
  }
148
164
 
149
165
  if (isString(schema)) {
@@ -27,6 +27,7 @@ export {
27
27
  runMcp,
28
28
  } from "./mcp-adapter.js";
29
29
 
30
- export { getBuiltinActions } from "./builtin/index.js";
30
+ export { getBuiltinActions, toNativeScript, completeWithScripts } from "./builtin/index.js";
31
+ export type { NativeScriptInput } from "./builtin/index.js";
31
32
 
32
33
  export { promptAndExecute } from "./tui-helpers.js";
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Provides:
5
5
  * - TypeBox to JSON Schema conversion (typeboxToJsonSchema)
6
+ * - TypeBox to Zod conversion for MCP SDK compatibility (typeboxToZod)
6
7
  * - BigInt string coercion for MCP input (coerceMcpInput)
7
8
  * - Lazy MCP SDK import with graceful error (getMcpSdk)
8
9
  * - MCP server creation from registered actions (createMcpServer)
@@ -279,6 +280,180 @@ export function coerceMcpInput(input: unknown, schema: TSchema): unknown {
279
280
  return input;
280
281
  }
281
282
 
283
+ // ---------------------------------------------------------------------------
284
+ // TypeBox to Zod conversion
285
+ // ---------------------------------------------------------------------------
286
+
287
+ /**
288
+ * Zod module shape (the parts we use from zod).
289
+ * Typed loosely since zod is a transitive dependency of @modelcontextprotocol/sdk,
290
+ * not a direct dependency of Sprinkles.
291
+ */
292
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
293
+ type ZodType = any;
294
+ interface IZodModule {
295
+ string: () => ZodType;
296
+ number: () => ZodType;
297
+ boolean: () => ZodType;
298
+ literal: (value: unknown) => ZodType;
299
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
300
+ object: (shape: Record<string, any>) => ZodType;
301
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
302
+ array: (item: any) => ZodType;
303
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
304
+ union: (members: any[]) => ZodType;
305
+ nullType: () => ZodType;
306
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
307
+ any: () => any;
308
+ }
309
+
310
+ let _zod: IZodModule | undefined;
311
+
312
+ /**
313
+ * Lazily import the Zod module.
314
+ * Zod is a transitive dependency of @modelcontextprotocol/sdk, so it's
315
+ * guaranteed to be available when MCP mode is used.
316
+ */
317
+ async function getZod(): Promise<IZodModule> {
318
+ if (_zod) return _zod;
319
+ try {
320
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
321
+ const mod = (await import("zod")) as any;
322
+ _zod = {
323
+ string: () => mod.string(),
324
+ number: () => mod.number(),
325
+ boolean: () => mod.boolean(),
326
+ literal: (v: unknown) => mod.literal(v),
327
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
328
+ object: (shape: Record<string, any>) => mod.object(shape),
329
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
330
+ array: (item: any) => mod.array(item),
331
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
332
+ union: (members: any[]) => mod.union(members),
333
+ nullType: () => mod.nullType(),
334
+ any: () => mod.any(),
335
+ };
336
+ return _zod;
337
+ } catch {
338
+ throw new Error(
339
+ "MCP mode requires zod (via @modelcontextprotocol/sdk). Install @modelcontextprotocol/sdk.",
340
+ );
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Convert a TypeBox schema to a Zod schema for MCP SDK compatibility.
346
+ *
347
+ * The MCP SDK (v1.27+) expects Zod schemas for tool input validation,
348
+ * not plain JSON Schema objects. This function converts TypeBox schemas
349
+ * to their Zod equivalents so the SDK correctly registers tool parameters.
350
+ *
351
+ * BigInt fields are represented as z.string() with a regex pattern since
352
+ * JSON (and thus MCP) has no BigInt type. The coerceMcpInput function
353
+ * handles converting these string values back to BigInt at call time.
354
+ */
355
+ export async function typeboxToZod(schema: TSchema): Promise<ZodType> {
356
+ const z = await getZod();
357
+ return _typeboxToZod(schema, z);
358
+ }
359
+
360
+ function _typeboxToZod(schema: TSchema, z: IZodModule): ZodType {
361
+ if (isBigInt(schema)) {
362
+ // BigInt -> string with regex pattern (JSON has no BigInt)
363
+ return z.string().regex(/^-?[0-9]+$/);
364
+ }
365
+
366
+ if (isString(schema)) {
367
+ let s = z.string();
368
+ if (schema.description) s = s.describe(schema.description);
369
+ return s;
370
+ }
371
+
372
+ if (isNumber(schema) || isInteger(schema)) {
373
+ let n = z.number();
374
+ if (schema.description) n = n.describe(schema.description);
375
+ return n;
376
+ }
377
+
378
+ if (isBoolean(schema)) {
379
+ let b = z.boolean();
380
+ if (schema.description) b = b.describe(schema.description);
381
+ return b;
382
+ }
383
+
384
+ if (isNull(schema)) {
385
+ return z.nullType();
386
+ }
387
+
388
+ if (isLiteral(schema)) {
389
+ return z.literal(schema.const);
390
+ }
391
+
392
+ if (isArray(schema)) {
393
+ const itemSchema = (schema as { items?: TSchema }).items;
394
+ if (itemSchema) {
395
+ return z.array(_typeboxToZod(itemSchema, z));
396
+ }
397
+ return z.array(z.any());
398
+ }
399
+
400
+ if (isObject(schema)) {
401
+ const properties = schema.properties as Record<string, TSchema>;
402
+ const shape: Record<string, ZodType> = {};
403
+
404
+ for (const [propName, propSchema] of Object.entries(properties)) {
405
+ let zodProp = _typeboxToZod(propSchema, z);
406
+ if (isOptional(propSchema)) {
407
+ zodProp = zodProp.optional();
408
+ }
409
+ shape[propName] = zodProp;
410
+ }
411
+
412
+ return z.object(shape);
413
+ }
414
+
415
+ if (isUnion(schema)) {
416
+ const members = schema.anyOf.map((member: TSchema) =>
417
+ _typeboxToZod(member, z),
418
+ );
419
+ if (members.length >= 2) {
420
+ return z.union(members);
421
+ }
422
+ return members[0] ?? z.any();
423
+ }
424
+
425
+ // Fallback: accept anything
426
+ return z.any();
427
+ }
428
+
429
+ /**
430
+ * Convert a TypeBox object schema to a Zod "raw shape" (Record<string, ZodType>).
431
+ * This is the format expected by the MCP SDK's tool() API.
432
+ */
433
+ export async function typeboxToZodShape(
434
+ schema: TSchema,
435
+ ): Promise<Record<string, ZodType>> {
436
+ const z = await getZod();
437
+
438
+ if (!isObject(schema)) {
439
+ // Non-object input schemas get wrapped as { input: zodSchema }
440
+ return { input: _typeboxToZod(schema, z) };
441
+ }
442
+
443
+ const properties = schema.properties as Record<string, TSchema>;
444
+ const shape: Record<string, ZodType> = {};
445
+
446
+ for (const [propName, propSchema] of Object.entries(properties)) {
447
+ let zodProp = _typeboxToZod(propSchema, z);
448
+ if (isOptional(propSchema)) {
449
+ zodProp = zodProp.optional();
450
+ }
451
+ shape[propName] = zodProp;
452
+ }
453
+
454
+ return shape;
455
+ }
456
+
282
457
  // ---------------------------------------------------------------------------
283
458
  // Lazy MCP SDK import
284
459
  // ---------------------------------------------------------------------------
@@ -364,15 +539,15 @@ export async function createMcpServer<S extends TSchema>(
364
539
  const actions = sprinkle.listActions() as AnyAction<S>[];
365
540
 
366
541
  for (const action of actions) {
367
- const inputSchema = typeboxToJsonSchema(action.inputSchema);
542
+ const zodShape = await typeboxToZodShape(action.inputSchema);
368
543
 
369
544
  // Register the action as an MCP tool.
370
- // The high-level McpServer.tool() API accepts:
371
- // tool(name, description, schema, handler)
545
+ // The high-level McpServer.tool() API accepts a Zod raw shape
546
+ // (Record<string, ZodType>) for input validation.
372
547
  server.tool(
373
548
  action.name,
374
549
  action.description,
375
- inputSchema,
550
+ zodShape,
376
551
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
377
552
  async (input: unknown): Promise<any> => {
378
553
  const coercedInput = coerceMcpInput(input, action.inputSchema);