diff --git a/README.md b/README.md index 84e2931c..417aff3b 100644 --- a/README.md +++ b/README.md @@ -122,12 +122,17 @@ Evolution SDK is built as a **single package** with a clean, modular structure t ``` evolution-sdk/ ├── 📦 packages/ -│ └── evolution/ # Main SDK package +│ ├── evolution/ # Main SDK package +│ │ ├── src/ +│ │ │ ├── Address.ts # Address utilities +│ │ │ ├── Transaction.ts # Transaction building +│ │ │ ├── Devnet/ # Development network tools +│ │ │ └── ... +│ │ └── dist/ # Compiled output +│ └── evolution-mcp/ # MCP server │ ├── src/ -│ │ ├── Address.ts # Address utilities -│ │ ├── Transaction.ts # Transaction building -│ │ ├── Devnet/ # Development network tools -│ │ └── ... +│ │ ├── server.ts # 66 MCP tools +│ │ └── bin.ts # HTTP entrypoint │ └── dist/ # Compiled output ├── docs/ # Documentation ├── turbo.json # Turbo configuration @@ -140,6 +145,7 @@ evolution-sdk/ | Package | Description | Status | Documentation | | -------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | | [`@evolution-sdk/evolution`](./packages/evolution) | Complete Cardano SDK with address management, transactions, and DevNet tools | In Development | [README](./packages/evolution/README.md) | +| [`@evolution-sdk/mcp`](./packages/evolution-mcp) | MCP server exposing 66 SDK tools to AI agents over HTTP | In Development | [README](./packages/evolution-mcp/README.md) | ### Core Features @@ -209,6 +215,9 @@ Evolution SDK provides **125+ core modules** plus SDK utilities, organized into ### Development Tools (2 modules) - `Devnet`, `DevnetDefault` - Local development network with custom configuration, automated testing, transaction simulation, and performance monitoring +### MCP Server (66 tools) +- `@evolution-sdk/mcp` - HTTP-based [Model Context Protocol](https://modelcontextprotocol.io) server at `localhost:10000/mcp` exposing the full SDK surface to AI agents (GitHub Copilot, Claude, Cursor, and any MCP client). Covers addresses, transactions, governance, smart contracts, CBOR codecs, key derivation, devnet management, and end-to-end transaction workflows. + ## Development ### Setting Up the Development Environment diff --git a/docs/content/docs/mcp/index.mdx b/docs/content/docs/mcp/index.mdx new file mode 100644 index 00000000..3dcedcad --- /dev/null +++ b/docs/content/docs/mcp/index.mdx @@ -0,0 +1,198 @@ +--- +title: MCP Server +description: Model Context Protocol server exposing Evolution SDK to AI agents +--- + +# MCP Server + +The Evolution SDK ships an HTTP-based [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that exposes the full SDK surface — addresses, transactions, governance, smart contracts, devnet management, and more — as callable tools for AI agents and LLM-powered workflows. + +## What is the MCP Server? + +`@evolution-sdk/mcp` wraps Evolution SDK functionality into **66 tools** that any MCP-compatible client can invoke over HTTP or stdio. The server starts automatically after installation and listens at `http://localhost:10000/mcp`, so AI assistants such as GitHub Copilot, Claude, Cursor, and other MCP clients can build, encode, sign, and submit Cardano transactions without writing code directly. + +The server supports two transports: +- **HTTP** — StreamableHTTP server for network-accessible MCP (default) +- **stdio** — JSON-RPC over stdin/stdout for clients that spawn the server process directly + +The server is stateless for pure SDK operations (codecs, hashing, address building) and stateful where needed (client sessions, transaction builder flows, devnet cluster management). + +## When to Use the MCP Server + +**AI-Assisted Development**: Let your coding assistant construct addresses, encode CBOR, build transactions, and query devnet state through natural language. + +**Rapid Prototyping**: Explore the SDK API surface interactively — build a transaction output, hash it, wrap it in a script reference — all through tool calls without compiling TypeScript. + +**Automated Pipelines**: Integrate Cardano operations into LLM-driven CI/CD or testing workflows that communicate via the MCP protocol. + +**Education & Exploration**: Understand how Cardano primitives fit together by asking an AI agent to walk through address creation, datum encoding, or governance proposal construction step by step. + +## Installation + +```bash +pnpm add @evolution-sdk/mcp +``` + +The package runs a `postinstall` bootstrap that registers and starts the server as a background process. After installation completes the HTTP server is reachable at: + +| Endpoint | URL | +|----------|-----| +| MCP | `http://localhost:10000/mcp` | +| Health | `http://localhost:10000/health` | + +If automatic startup cannot be completed (e.g. inside a Docker build), installation still succeeds and prints a manual fallback command: + +```bash +# HTTP server (default) +node packages/evolution-mcp/dist/bin.js serve + +# Stdio transport +node packages/evolution-mcp/dist/bin.js stdio +``` + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `EVOLUTION_MCP_HOST` | `127.0.0.1` | Bind address | +| `EVOLUTION_MCP_PORT` | `10000` | Bind port | +| `EVOLUTION_MCP_PATH` | `/mcp` | MCP route | +| `EVOLUTION_MCP_HEALTH_PATH` | `/health` | Health check route | +| `EVOLUTION_MCP_SKIP_POSTINSTALL` | — | Set to `1` to skip install-time bootstrap | +| `EVOLUTION_MCP_POSTINSTALL_STRICT` | — | Set to `1` to fail install if bootstrap fails | + +## Tool Categories + +The 66 tools are organized into the following categories: + +### Meta / Introspection (3 tools) +Query SDK version, enumerate exported modules, destroy session handles, and retrieve server runtime statistics. + +### Codecs & Encoding (8 tools) +Stateless CBOR encode/decode for Address, Assets, Plutus Data, identifiers, and hashes. A **generic typed-export codec** covering 40+ SDK modules (Transaction, WitnessSet, Script, Certificate, Value, etc.) with `fromCBORHex`/`toCBORHex`. Bech32/bytes encoding. Structured Plutus data codecs for OutputReference, Credential, Address, Lovelace, and CIP-68 metadata. + +### Workflow (7 tools) +End-to-end transaction lifecycle — create client sessions, attach providers and wallets (via unified `client_attach`), invoke provider queries, open transaction builders, build with optional Plutus evaluation, and sign/submit via unified `result_call`. + +### Cryptography (5 tools) +BIP-39 mnemonic generation and BIP32-Ed25519 key derivation, BIP32 HD key export/import, CIP-8/CIP-30 message signing and verification, Ed25519 signature encode/decode/validate. + +### Governance & Certificates (10 tools) +Anchors, Certificates (pre-Conway and Conway era), VotingProcedures, GovernanceActions (all CIP-1694 types), ProposalProcedures, DRep tools (creation, Bech32, CBOR), DRep certificates, Committee certificates, Constitution, and Protocol Parameter Updates. + +### Transaction Primitives (9 tools) +TransactionInput, TransactionOutput, TransactionBody construction, Mint for minting/burning tokens, Withdrawals for reward claiming, Redeemer/ExUnits building, Redeemers collection (Conway map format), ProposalProcedures collection, and ScriptRef tag-24 wrapping. + +### Value & Assets (5 tools) +Create ADA-only or multi-asset Values with arithmetic, Assets construction and merge, CIP-67 unit/label tools, Coin arithmetic with overflow protection, and Plutus script-level Value maps. + +### Scripts (4 tools) +Native script building/analysis, Script union wrapping (NativeScript/PlutusV1-V3), UPLC script inspection and parameter application, and UPLC evaluator discovery. + +### Address Types (3 tools) +Build Base/Enterprise/Reward addresses from credentials, PointerAddress construction, and Byron-era address decoding. + +### Data & Hashing (2 tools) +Plutus Data construction (`constr`/`int`/`bytes`/`list`/`map`) with pattern matching, and Blake2b-256 hashing. + +### Blueprints (2 tools) +CIP-57 Plutus blueprint parsing and TypeScript codegen. + +### Network & Time (2 tools) +Network name/ID mapping and slot-to-Unix/Unix-to-slot conversion with per-network config. + +### Metadata & Credentials (2 tools) +Transaction metadata/AuxiliaryData construction and credential creation (key-hash/script-hash). + +### Other (3 tools) +UTxO set operations, full PoolParams for stake pool registration, and fee validation. + +### Devnet (1 tool) +Docker-based local Cardano cluster management — create, start, stop, remove, query genesis UTxOs, query epochs, execute commands, and inspect default config. + +## Usage with MCP Clients + +### Claude Desktop + +Add the following to your Claude Desktop MCP configuration: + +```json +{ + "mcpServers": { + "evolution-sdk": { + "url": "http://localhost:10000/mcp" + } + } +} +``` + +### VS Code (GitHub Copilot) + +Add to your VS Code `settings.json`: + +```json +{ + "mcp": { + "servers": { + "evolution-sdk": { + "type": "http", + "url": "http://localhost:10000/mcp" + } + } + } +} +``` + +### Cursor + +Add to your Cursor MCP settings: + +```json +{ + "mcpServers": { + "evolution-sdk": { + "url": "http://localhost:10000/mcp" + } + } +} +``` + +### Any MCP Client + +The server speaks standard MCP over both HTTP (StreamableHTTP) and stdio (JSON-RPC over stdin/stdout). For HTTP clients, point at `http://localhost:10000/mcp`. For stdio clients, configure the command: + +```json +{ + "mcpServers": { + "evolution-sdk": { + "command": "node", + "args": ["packages/evolution-mcp/dist/bin.js", "stdio"] + } + } +} +``` + +## Example Interactions + +Once connected, you can ask your AI assistant things like: + +- *"Create a testnet enterprise address from this key hash"* +- *"Encode this Plutus datum to CBOR hex"* +- *"Build a transaction with these inputs and outputs, then hash it"* +- *"Start a local devnet cluster and fund my test address"* +- *"Parse this CIP-57 blueprint and generate TypeScript bindings"* +- *"Build a governance proposal for a treasury withdrawal"* +- *"What modules does Evolution SDK export?"* + +The AI agent translates your request into the appropriate tool calls and returns structured results. + +## Package Coverage + +The MCP server covers all four workspace packages: + +| Package | Description | +|---------|-------------| +| `@evolution-sdk/evolution` | Core SDK — addresses, transactions, governance, cryptography | +| `@evolution-sdk/aiken-uplc` | Aiken UPLC evaluator | +| `@evolution-sdk/scalus-uplc` | Scalus UPLC evaluator | +| `@evolution-sdk/devnet` | Local Cardano development network via Docker | diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json index 5bfd59ea..5a691606 100644 --- a/docs/content/docs/meta.json +++ b/docs/content/docs/meta.json @@ -16,6 +16,7 @@ "staking", "governance", "devnet", + "mcp", "testing", "advanced", "modules" diff --git a/docs/content/docs/modules/CBOR.mdx b/docs/content/docs/modules/CBOR.mdx index f42af70b..2d0ae087 100644 --- a/docs/content/docs/modules/CBOR.mdx +++ b/docs/content/docs/modules/CBOR.mdx @@ -20,17 +20,41 @@ parent: Modules - [CML_DATA_DEFAULT_OPTIONS](#cml_data_default_options) - [CML_DEFAULT_OPTIONS](#cml_default_options) - [STRUCT_FRIENDLY_OPTIONS](#struct_friendly_options) +- [decoding](#decoding) + - [decodeItemWithOffset](#decodeitemwithoffset) - [encoding](#encoding) - [toCBORBytes](#tocborbytes) + - [toCBORBytesWithFormat](#tocborbyteswithformat) - [toCBORHex](#tocborhex) + - [toCBORHexWithFormat](#tocborhexwithformat) +- [equality](#equality) + - [equals](#equals) - [errors](#errors) - [CBORError (class)](#cborerror-class) - [model](#model) + - [BoundedBytes](#boundedbytes) + - [ByteSize (type alias)](#bytesize-type-alias) - [CBOR (type alias)](#cbor-type-alias) + - [CBORFormat (type alias)](#cborformat-type-alias) + - [CBORFormat (namespace)](#cborformat-namespace) + - [Array (type alias)](#array-type-alias) + - [Bytes (type alias)](#bytes-type-alias) + - [Map (type alias)](#map-type-alias) + - [NInt (type alias)](#nint-type-alias) + - [Simple (type alias)](#simple-type-alias) + - [Tag (type alias)](#tag-type-alias) + - [Text (type alias)](#text-type-alias) + - [UInt (type alias)](#uint-type-alias) - [CodecOptions (type alias)](#codecoptions-type-alias) + - [DecodedWithFormat (type alias)](#decodedwithformat-type-alias) + - [LengthEncoding (type alias)](#lengthencoding-type-alias) + - [StringEncoding (type alias)](#stringencoding-type-alias) - [parsing](#parsing) - [fromCBORBytes](#fromcborbytes) + - [fromCBORBytesWithFormat](#fromcborbyteswithformat) - [fromCBORHex](#fromcborhex) + - [fromCBORHexWithFormat](#fromcborhexwithformat) + - [internalDecodeWithFormatSync](#internaldecodewithformatsync) - [schemas](#schemas) - [CBORSchema](#cborschema) - [FromBytes](#frombytes) @@ -68,14 +92,17 @@ parent: Modules ## AIKEN_DEFAULT_OPTIONS -Aiken-compatible CBOR encoding options +Aiken-compatible CBOR encoding options. -Matches the encoding used by Aiken's cbor.serialise(): +Matches the encoding produced by `cbor.serialise()` in Aiken: -- Indefinite-length arrays (9f...ff) +- Indefinite-length arrays (`9f...ff`) - Maps encoded as arrays of pairs (not CBOR maps) -- Strings as bytearrays (major type 2, not 3) -- Constructor tags: 121-127 for indices 0-6, then 1280+ for 7+ +- Strings as byte arrays (major type 2, not 3) +- Constructor tags: 121–127 for indices 0–6, then 1280+ for 7+ + +PlutusData byte strings are chunked per the Conway `bounded_bytes` rule +via the `BoundedBytes` CBOR node, independent of these codec options. **Signature** @@ -169,7 +196,11 @@ Added in v1.0.0 ## CML_DATA_DEFAULT_OPTIONS -Default CBOR encoding option for Data +Default CBOR encoding options for PlutusData. + +Uses indefinite-length arrays and maps. The `bounded_bytes` constraint +(Conway CDDL: byte strings ≤ 64 bytes) is enforced at the data-type layer +via the `BoundedBytes` CBOR node, independent of these codec options. **Signature** @@ -203,6 +234,25 @@ export declare const STRUCT_FRIENDLY_OPTIONS: CodecOptions Added in v2.0.0 +# decoding + +## decodeItemWithOffset + +Decode a single CBOR item at a given byte offset, returning the decoded value and the new offset. +Useful for extracting raw byte slices from CBOR-encoded data without re-encoding. + +**Signature** + +```ts +export declare const decodeItemWithOffset: ( + data: Uint8Array, + offset: number, + options?: CodecOptions +) => { item: CBOR; newOffset: number } +``` + +Added in v2.0.0 + # encoding ## toCBORBytes @@ -217,6 +267,18 @@ export declare const toCBORBytes: (value: CBOR, options?: CodecOptions) => Uint8 Added in v1.0.0 +## toCBORBytesWithFormat + +Convert a CBOR value to CBOR bytes using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORBytesWithFormat: (value: CBOR, format: CBORFormat) => Uint8Array +``` + +Added in v2.0.0 + ## toCBORHex Convert a CBOR value to CBOR hex string. @@ -229,6 +291,36 @@ export declare const toCBORHex: (value: CBOR, options?: CodecOptions) => string Added in v1.0.0 +## toCBORHexWithFormat + +Convert a CBOR value to CBOR hex string using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORHexWithFormat: (value: CBOR, format: CBORFormat) => string +``` + +Added in v2.0.0 + +# equality + +## equals + +Schema-derived structural equivalence for CBOR values. +Handles Uint8Array, Array, Map, Tag and all primitives via the +recursive CBORSchema definition — no hand-rolled comparison needed. + +Derived once at module init; at call time it's a plain function. + +**Signature** + +```ts +export declare const equals: (a: CBOR, b: CBOR) => boolean +``` + +Added in v2.0.0 + # errors ## CBORError (class) @@ -245,6 +337,41 @@ Added in v1.0.0 # model +## BoundedBytes + +`BoundedBytes` CBOR node — represents a PlutusData byte string that must comply +with the Conway CDDL constraint `bounded_bytes = bytes .size (0..64)`. + +The encoding rule is unconditional and options-independent: + +- ≤ 64 bytes → definite-length CBOR bytes +- > 64 bytes → indefinite-length 64-byte chunked byte string (`0x5f` + chunks + `0xff`) + +Use `BoundedBytes.make` to construct the node; the encoder handles the rest. + +**Signature** + +```ts +export declare const BoundedBytes: { + readonly make: (bytes: Uint8Array) => CBOR + readonly is: (value: CBOR) => value is { _tag: "BoundedBytes"; bytes: Uint8Array } +} +``` + +Added in v2.0.0 + +## ByteSize (type alias) + +Width of a CBOR integer argument: inline (0), 1-byte, 2-byte, 4-byte, or 8-byte. + +**Signature** + +```ts +export type ByteSize = 0 | 1 | 2 | 4 | 8 +``` + +Added in v2.0.0 + ## CBOR (type alias) Type representing a CBOR value with simplified, non-tagged structure @@ -263,11 +390,131 @@ export type CBOR = | boolean // boolean values | null // null value | undefined // undefined value - | number + | number // floating point numbers + | { _tag: "BoundedBytes"; bytes: Uint8Array } ``` Added in v1.0.0 +## CBORFormat (type alias) + +Tagged discriminated union capturing how each CBOR node was originally +serialized. Every variant carries a `_tag` discriminant. Encoding-detail +fields are optional — absent means "use canonical / minimal default". + +**Signature** + +```ts +export type CBORFormat = + | CBORFormat.UInt + | CBORFormat.NInt + | CBORFormat.Bytes + | CBORFormat.Text + | CBORFormat.Array + | CBORFormat.Map + | CBORFormat.Tag + | CBORFormat.Simple +``` + +Added in v2.0.0 + +## CBORFormat (namespace) + +Added in v2.0.0 + +### Array (type alias) + +Array (major 4). `length` absent → definite, minimal length header. + +**Signature** + +```ts +export type Array = { + readonly _tag: "array" + readonly length?: LengthEncoding + readonly children: ReadonlyArray +} +``` + +### Bytes (type alias) + +Byte string (major 2). `encoding` absent → definite, minimal length. + +**Signature** + +```ts +export type Bytes = { readonly _tag: "bytes"; readonly encoding?: StringEncoding } +``` + +### Map (type alias) + +Map (major 5). `keyOrder` stores CBOR-encoded key bytes for serializable ordering. + +**Signature** + +```ts +export type Map = { + readonly _tag: "map" + readonly length?: LengthEncoding + readonly keyOrder?: ReadonlyArray + readonly entries: ReadonlyArray +} +``` + +### NInt (type alias) + +Negative integer (major 1). `byteSize` absent → minimal encoding. + +**Signature** + +```ts +export type NInt = { readonly _tag: "nint"; readonly byteSize?: ByteSize } +``` + +### Simple (type alias) + +Simple value or float (major 7). No encoding choices to preserve. + +**Signature** + +```ts +export type Simple = { readonly _tag: "simple" } +``` + +### Tag (type alias) + +Tag (major 6). `width` absent → minimal tag header. + +**Signature** + +```ts +export type Tag = { + readonly _tag: "tag" + readonly width?: ByteSize + readonly child: CBORFormat +} +``` + +### Text (type alias) + +Text string (major 3). `encoding` absent → definite, minimal length. + +**Signature** + +```ts +export type Text = { readonly _tag: "text"; readonly encoding?: StringEncoding } +``` + +### UInt (type alias) + +Unsigned integer (major 0). `byteSize` absent → minimal encoding. + +**Signature** + +```ts +export type UInt = { readonly _tag: "uint"; readonly byteSize?: ByteSize } +``` + ## CodecOptions (type alias) CBOR codec configuration options @@ -295,6 +542,50 @@ export type CodecOptions = Added in v1.0.0 +## DecodedWithFormat (type alias) + +Decoded value paired with its captured root format tree. + +**Signature** + +```ts +export type DecodedWithFormat = { + value: A + format: CBORFormat +} +``` + +Added in v2.0.0 + +## LengthEncoding (type alias) + +Container length encoding style captured during decode. + +**Signature** + +```ts +export type LengthEncoding = { readonly tag: "indefinite" } | { readonly tag: "definite"; readonly byteSize: ByteSize } +``` + +Added in v2.0.0 + +## StringEncoding (type alias) + +Byte/text string encoding style captured during decode. + +**Signature** + +```ts +export type StringEncoding = + | { readonly tag: "definite"; readonly byteSize: ByteSize } + | { + readonly tag: "indefinite" + readonly chunks: ReadonlyArray<{ readonly length: number; readonly byteSize: ByteSize }> + } +``` + +Added in v2.0.0 + # parsing ## fromCBORBytes @@ -309,6 +600,18 @@ export declare const fromCBORBytes: (bytes: Uint8Array, options?: CodecOptions) Added in v1.0.0 +## fromCBORBytesWithFormat + +Parse a CBOR value from CBOR bytes and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORBytesWithFormat: (bytes: Uint8Array) => DecodedWithFormat +``` + +Added in v2.0.0 + ## fromCBORHex Parse a CBOR value from CBOR hex string. @@ -321,6 +624,30 @@ export declare const fromCBORHex: (hex: string, options?: CodecOptions) => CBOR Added in v1.0.0 +## fromCBORHexWithFormat + +Parse a CBOR value from CBOR hex string and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORHexWithFormat: (hex: string) => DecodedWithFormat +``` + +Added in v2.0.0 + +## internalDecodeWithFormatSync + +Decode CBOR bytes and return both the decoded value and the root format tree. + +**Signature** + +```ts +export declare const internalDecodeWithFormatSync: (data: Uint8Array) => DecodedWithFormat +``` + +Added in v2.0.0 + # schemas ## CBORSchema @@ -385,6 +712,7 @@ export declare const match: ( null: () => R undefined: () => R float: (value: number) => R + boundedBytes: (value: Uint8Array) => R } ) => R ``` @@ -524,7 +852,7 @@ export declare const internalDecodeSync: (data: Uint8Array, options?: CodecOptio **Signature** ```ts -export declare const internalEncodeSync: (value: CBOR, options?: CodecOptions) => Uint8Array +export declare const internalEncodeSync: (value: CBOR, options?: CodecOptions, fmt?: CBORFormat) => Uint8Array ``` ## isArray diff --git a/docs/content/docs/modules/Data.mdx b/docs/content/docs/modules/Data.mdx index d693e788..4dd2e1c6 100644 --- a/docs/content/docs/modules/Data.mdx +++ b/docs/content/docs/modules/Data.mdx @@ -180,8 +180,9 @@ Added in v2.0.0 ## equals -Deep structural equality for Plutus Data values. -Handles maps, lists, ints, bytes, and constrs. +Schema-derived structural equality for Plutus Data values. +Handles maps, lists, ints, bytes, and constrs via the +recursive DataSchema definition — no hand-rolled comparison needed. **Signature** diff --git a/docs/content/docs/modules/PrivateKey.mdx b/docs/content/docs/modules/PrivateKey.mdx index dc82ab72..86ddb65f 100644 --- a/docs/content/docs/modules/PrivateKey.mdx +++ b/docs/content/docs/modules/PrivateKey.mdx @@ -13,13 +13,14 @@ parent: Modules - [arbitrary](#arbitrary) - [arbitrary](#arbitrary-1) - [bip32](#bip32) - - [derive](#derive) + - [~~derive~~](#derive) - [bip39](#bip39) - - [fromMnemonic](#frommnemonic) + - [~~fromMnemonic~~](#frommnemonic) - [generateMnemonic](#generatemnemonic) - [validateMnemonic](#validatemnemonic) - [cardano](#cardano) - - [CardanoPath](#cardanopath) + - [~~CardanoPath~~](#cardanopath) + - [fromMnemonicCardano](#frommnemoniccardano) - [cryptography](#cryptography) - [sign](#sign) - [toPublicKey](#topublickey) @@ -70,10 +71,12 @@ Added in v2.0.0 # bip32 -## derive +## ~~derive~~ Derive a child private key using BIP32 path (sync version that throws PrivateKeyError). -All errors are normalized to PrivateKeyError with contextual information. + +**WARNING**: This uses secp256k1 BIP32 derivation (`@scure/bip32`), NOT Cardano's +BIP32-Ed25519. For Cardano key derivation, use `fromMnemonicCardano` instead. **Signature** @@ -85,10 +88,12 @@ Added in v2.0.0 # bip39 -## fromMnemonic +## ~~fromMnemonic~~ Create a PrivateKey from a mnemonic phrase (sync version that throws PrivateKeyError). -All errors are normalized to PrivateKeyError with contextual information. + +**WARNING**: This uses secp256k1 BIP32 derivation (`@scure/bip32`), NOT Cardano's +BIP32-Ed25519. For Cardano key derivation, use `fromMnemonicCardano` instead. **Signature** @@ -124,10 +129,15 @@ Added in v2.0.0 # cardano -## CardanoPath +## ~~CardanoPath~~ Cardano BIP44 derivation path utilities. +**WARNING**: These paths are only useful with BIP32-Ed25519 derivation +(`Bip32PrivateKey`). Using them with `derive` (which uses secp256k1 BIP32) +will produce incorrect keys. Use `fromMnemonicCardano` or +`Bip32PrivateKey.CardanoPath` instead. + **Signature** ```ts @@ -140,6 +150,41 @@ export declare const CardanoPath: { Added in v2.0.0 +## fromMnemonicCardano + +Derive a Cardano payment or stake key from a mnemonic using BIP32-Ed25519. + +This is the correct way to derive Cardano keys from a mnemonic. It uses the +Icarus/V2 BIP32-Ed25519 derivation scheme, matching CML and cardano-cli behavior. + +**Signature** + +```ts +export declare const fromMnemonicCardano: ( + mnemonic: string, + options?: { account?: number; role?: 0 | 2; index?: number; password?: string } +) => PrivateKey +``` + +**Example** + +```ts +import * as PrivateKey from "@evolution-sdk/evolution/PrivateKey" + +const mnemonic = PrivateKey.generateMnemonic() + +// Payment key (default: account 0, index 0) +const paymentKey = PrivateKey.fromMnemonicCardano(mnemonic) + +// Stake key +const stakeKey = PrivateKey.fromMnemonicCardano(mnemonic, { role: 2 }) + +// Custom account/index +const key = PrivateKey.fromMnemonicCardano(mnemonic, { account: 1, index: 3 }) +``` + +Added in v2.0.0 + # cryptography ## sign diff --git a/docs/content/docs/modules/Redeemers.mdx b/docs/content/docs/modules/Redeemers.mdx index 5a6f3892..5292b895 100644 --- a/docs/content/docs/modules/Redeemers.mdx +++ b/docs/content/docs/modules/Redeemers.mdx @@ -12,19 +12,34 @@ parent: Modules - [arbitrary](#arbitrary) - [arbitrary](#arbitrary-1) +- [constructors](#constructors) + - [makeRedeemerMap](#makeredeemermap) - [encoding](#encoding) - [toCBORBytes](#tocborbytes) - [toCBORBytesMap](#tocborbytesmap) - [toCBORHex](#tocborhex) - [toCBORHexMap](#tocborhexmap) - [model](#model) - - [Format (type alias)](#format-type-alias) - - [Redeemers (class)](#redeemers-class) + - [RedeemerArray (class)](#redeemerarray-class) + - [toArray (method)](#toarray-method) - [toJSON (method)](#tojson-method) - [toString (method)](#tostring-method) - [[Inspectable.NodeInspectSymbol] (method)](#inspectablenodeinspectsymbol-method) - [[Equal.symbol] (method)](#equalsymbol-method) - [[Hash.symbol] (method)](#hashsymbol-method) + - [RedeemerKey (type alias)](#redeemerkey-type-alias) + - [RedeemerMap (class)](#redeemermap-class) + - [get (method)](#get-method) + - [toArray (method)](#toarray-method-1) + - [toJSON (method)](#tojson-method-1) + - [toString (method)](#tostring-method-1) + - [[Inspectable.NodeInspectSymbol] (method)](#inspectablenodeinspectsymbol-method-1) + - [[Equal.symbol] (method)](#equalsymbol-method-1) + - [[Hash.symbol] (method)](#hashsymbol-method-1) + - [RedeemerValue (class)](#redeemervalue-class) + - [[Equal.symbol] (method)](#equalsymbol-method-2) + - [[Hash.symbol] (method)](#hashsymbol-method-2) + - [Redeemers (type alias)](#redeemers-type-alias) - [parsing](#parsing) - [fromCBORBytes](#fromcborbytes) - [fromCBORBytesMap](#fromcborbytesmap) @@ -41,6 +56,9 @@ parent: Modules - [FromCDDL](#fromcddl) - [FromMapCDDL](#frommapcddl) - [MapCDDLSchema](#mapcddlschema) + - [Redeemers](#redeemers) +- [utilities](#utilities) + - [keyToString](#keytostring) --- @@ -48,12 +66,26 @@ parent: Modules ## arbitrary -FastCheck arbitrary for Redeemers. +FastCheck arbitrary for Redeemers — generates both map and array variants. **Signature** ```ts -export declare const arbitrary: FastCheck.Arbitrary +export declare const arbitrary: FastCheck.Arbitrary +``` + +Added in v2.0.0 + +# constructors + +## makeRedeemerMap + +Create a `RedeemerMap` from an array of `Redeemer` objects. + +**Signature** + +```ts +export declare const makeRedeemerMap: (redeemers: ReadonlyArray) => RedeemerMap ``` Added in v2.0.0 @@ -62,90 +94,183 @@ Added in v2.0.0 ## toCBORBytes -Encode Redeemers to CBOR bytes (array format). +Encode to CBOR bytes (array format). **Signature** ```ts -export declare const toCBORBytes: (data: Redeemers, options?: CBOR.CodecOptions) => any +export declare const toCBORBytes: (data: RedeemerArray, options?: CBOR.CodecOptions) => any ``` Added in v2.0.0 ## toCBORBytesMap -Encode Redeemers to CBOR bytes (map format). +Encode to CBOR bytes (map format). **Signature** ```ts -export declare const toCBORBytesMap: (data: Redeemers, options?: CBOR.CodecOptions) => any +export declare const toCBORBytesMap: (data: RedeemerMap, options?: CBOR.CodecOptions) => any ``` Added in v2.0.0 ## toCBORHex -Encode Redeemers to CBOR hex string (array format). +Encode to CBOR hex string (array format). **Signature** ```ts -export declare const toCBORHex: (data: Redeemers, options?: CBOR.CodecOptions) => string +export declare const toCBORHex: (data: RedeemerArray, options?: CBOR.CodecOptions) => string ``` Added in v2.0.0 ## toCBORHexMap -Encode Redeemers to CBOR hex string (map format). +Encode to CBOR hex string (map format). **Signature** ```ts -export declare const toCBORHexMap: (data: Redeemers, options?: CBOR.CodecOptions) => string +export declare const toCBORHexMap: (data: RedeemerMap, options?: CBOR.CodecOptions) => string ``` Added in v2.0.0 # model -## Format (type alias) +## RedeemerArray (class) -Encoding format for redeemers collection. +Redeemers in legacy array format. -Conway CDDL supports two formats: +Mirrors the CDDL: ``` -; Flat Array support is included for backwards compatibility and -; will be removed in the next era. It is recommended for tools to -; adopt using a Map instead of Array going forward. -redeemers = - [ + redeemer ] - / { + [tag : redeemer_tag, index : uint .size 4] => [ data : plutus_data, ex_units : ex_units ] } +[ + redeemer ] ``` -- "array": Legacy flat array format - backwards compatible, will be deprecated -- "map": New map format - recommended for Conway+ +Backwards compatible — will be deprecated in the next era. +Prefer `RedeemerMap` for new transactions. **Signature** ```ts -export type Format = "array" | "map" +export declare class RedeemerArray ``` Added in v2.0.0 -## Redeemers (class) +### toArray (method) + +Convert to an array of `Redeemer` objects (identity for array format). -Redeemers collection based on Conway CDDL specification. +**Signature** + +```ts +toArray(): ReadonlyArray +``` -Represents a collection of redeemers that can be encoded in either array or map format. +Added in v2.0.0 + +### toJSON (method) **Signature** ```ts -export declare class Redeemers +toJSON() +``` + +### toString (method) + +**Signature** + +```ts +toString(): string +``` + +### [Inspectable.NodeInspectSymbol] (method) + +**Signature** + +```ts +[Inspectable.NodeInspectSymbol](): unknown +``` + +### [Equal.symbol] (method) + +**Signature** + +```ts +[Equal.symbol](that: unknown): boolean +``` + +### [Hash.symbol] (method) + +**Signature** + +```ts +[Hash.symbol](): number +``` + +## RedeemerKey (type alias) + +A redeemer map key: `[tag, index]`. + +Mirrors the CDDL: `[tag : redeemer_tag, index : uint .size 4]` + +**Signature** + +```ts +export type RedeemerKey = readonly [Redeemer.RedeemerTag, bigint] +``` + +Added in v2.0.0 + +## RedeemerMap (class) + +Redeemers in map format (Conway recommended). + +Mirrors the CDDL exactly: + +``` +{ + [tag : redeemer_tag, index : uint .size 4] => [ data : plutus_data, ex_units : ex_units ] } +``` + +The map is keyed by `[tag, index]` tuples. Note: JS Map uses reference +equality for non-primitive keys, so lookups by tuple won't work — use +`get()` or `toArray()` helpers instead. + +**Signature** + +```ts +export declare class RedeemerMap +``` + +Added in v2.0.0 + +### get (method) + +Look up a redeemer entry by tag and index. + +**Signature** + +```ts +get(tag: Redeemer.RedeemerTag, index: bigint): RedeemerValue | undefined +``` + +Added in v2.0.0 + +### toArray (method) + +Convert to an array of `Redeemer` objects (convenience for consumers). + +**Signature** + +```ts +toArray(): ReadonlyArray ``` Added in v2.0.0 @@ -190,52 +315,94 @@ toString(): string [Hash.symbol](): number ``` +## RedeemerValue (class) + +A redeemer map entry value: `[data, ex_units]`. + +Mirrors the CDDL: `[data : plutus_data, ex_units : ex_units]` + +**Signature** + +```ts +export declare class RedeemerValue +``` + +Added in v2.0.0 + +### [Equal.symbol] (method) + +**Signature** + +```ts +[Equal.symbol](that: unknown): boolean +``` + +### [Hash.symbol] (method) + +**Signature** + +```ts +[Hash.symbol](): number +``` + +## Redeemers (type alias) + +Union type: `RedeemerMap | RedeemerArray` + +**Signature** + +```ts +export type Redeemers = typeof Redeemers.Type +``` + +Added in v2.0.0 + # parsing ## fromCBORBytes -Parse Redeemers from CBOR bytes (array format). +Parse from CBOR bytes (array format). **Signature** ```ts -export declare const fromCBORBytes: (bytes: Uint8Array, options?: CBOR.CodecOptions) => Redeemers +export declare const fromCBORBytes: (bytes: Uint8Array, options?: CBOR.CodecOptions) => RedeemerArray ``` Added in v2.0.0 ## fromCBORBytesMap -Parse Redeemers from CBOR bytes (map format). +Parse from CBOR bytes (map format). **Signature** ```ts -export declare const fromCBORBytesMap: (bytes: Uint8Array, options?: CBOR.CodecOptions) => Redeemers +export declare const fromCBORBytesMap: (bytes: Uint8Array, options?: CBOR.CodecOptions) => RedeemerMap ``` Added in v2.0.0 ## fromCBORHex -Parse Redeemers from CBOR hex string (array format). +Parse from CBOR hex string (array format). **Signature** ```ts -export declare const fromCBORHex: (hex: string, options?: CBOR.CodecOptions) => Redeemers +export declare const fromCBORHex: (hex: string, options?: CBOR.CodecOptions) => RedeemerArray ``` Added in v2.0.0 ## fromCBORHexMap -Parse Redeemers from CBOR hex string (map format). +Parse from CBOR hex string (map format). **Signature** ```ts -export declare const fromCBORHexMap: (hex: string, options?: CBOR.CodecOptions) => Redeemers +export declare const fromCBORHexMap: (hex: string, options?: CBOR.CodecOptions) => RedeemerMap ``` Added in v2.0.0 @@ -244,9 +411,7 @@ Added in v2.0.0 ## ArrayCDDLSchema -CDDL schema for Redeemers in array format. - -`redeemers = [ + redeemer ]` +CDDL schema for array format: `[ + redeemer ]` **Signature** @@ -267,19 +432,16 @@ Added in v2.0.0 ## CDDLSchema -Default CDDL schema for Redeemers (array format). +Default CDDL schema (map format — Conway recommended). **Signature** ```ts -export declare const CDDLSchema: Schema.Array$< - Schema.Tuple< - [ - Schema.SchemaClass, - Schema.SchemaClass, - Schema.Schema, - Schema.Tuple2 - ] +export declare const CDDLSchema: Schema.MapFromSelf< + Schema.Tuple2, + Schema.Tuple2< + Schema.Schema, + Schema.Tuple2 > > ``` @@ -288,7 +450,7 @@ Added in v2.0.0 ## FromArrayCDDL -CDDL transformation schema for Redeemers array format. +CDDL transformation for array format → `RedeemerArray`. **Signature** @@ -304,7 +466,7 @@ export declare const FromArrayCDDL: Schema.transformOrFail< ] > >, - Schema.SchemaClass, + Schema.SchemaClass, never > ``` @@ -313,7 +475,7 @@ Added in v2.0.0 ## FromCBORBytes -CBOR bytes transformation schema for Redeemers (array format). +CBOR bytes schema for array format. **Signature** @@ -337,7 +499,7 @@ export declare const FromCBORBytes: ( ] > >, - Schema.SchemaClass, + Schema.SchemaClass, never > > @@ -347,7 +509,7 @@ Added in v2.0.0 ## FromCBORBytesMap -CBOR bytes transformation schema for Redeemers (map format). +CBOR bytes schema for map format. **Signature** @@ -361,14 +523,14 @@ export declare const FromCBORBytesMap: ( never >, Schema.transformOrFail< - Schema.Map$< + Schema.MapFromSelf< Schema.Tuple2, Schema.Tuple2< Schema.Schema, Schema.Tuple2 > >, - Schema.SchemaClass, + Schema.SchemaClass, never > > @@ -378,7 +540,7 @@ Added in v2.0.0 ## FromCBORHex -CBOR hex transformation schema for Redeemers (array format). +CBOR hex schema for array format. **Signature** @@ -404,7 +566,7 @@ export declare const FromCBORHex: ( ] > >, - Schema.SchemaClass, + Schema.SchemaClass, never > > @@ -415,7 +577,7 @@ Added in v2.0.0 ## FromCBORHexMap -CBOR hex transformation schema for Redeemers (map format). +CBOR hex schema for map format. **Signature** @@ -431,14 +593,14 @@ export declare const FromCBORHexMap: ( never >, Schema.transformOrFail< - Schema.Map$< + Schema.MapFromSelf< Schema.Tuple2, Schema.Tuple2< Schema.Schema, Schema.Tuple2 > >, - Schema.SchemaClass, + Schema.SchemaClass, never > > @@ -449,23 +611,20 @@ Added in v2.0.0 ## FromCDDL -Default CDDL transformation (array format). +Default CDDL transformation (map format). **Signature** ```ts export declare const FromCDDL: Schema.transformOrFail< - Schema.Array$< - Schema.Tuple< - [ - Schema.SchemaClass, - Schema.SchemaClass, - Schema.Schema, - Schema.Tuple2 - ] + Schema.MapFromSelf< + Schema.Tuple2, + Schema.Tuple2< + Schema.Schema, + Schema.Tuple2 > >, - Schema.SchemaClass, + Schema.SchemaClass, never > ``` @@ -474,20 +633,20 @@ Added in v2.0.0 ## FromMapCDDL -CDDL transformation schema for Redeemers map format. +CDDL transformation for map format → `RedeemerMap`. **Signature** ```ts export declare const FromMapCDDL: Schema.transformOrFail< - Schema.Map$< + Schema.MapFromSelf< Schema.Tuple2, Schema.Tuple2< Schema.Schema, Schema.Tuple2 > >, - Schema.SchemaClass, + Schema.SchemaClass, never > ``` @@ -496,14 +655,16 @@ Added in v2.0.0 ## MapCDDLSchema -CDDL schema for Redeemers in map format. +CDDL schema for map format: `{ + [tag, index] => [data, ex_units] }` -`{ + [tag, index] => [data, ex_units] }` +Uses `MapFromSelf` (not `Map`) so the Encoded type is a JS Map — matching +how `CBOR.FromBytes` represents CBOR major-type-5 maps at runtime. +This is the same pattern used by Withdrawals, Mint, MultiAsset, CostModel. **Signature** ```ts -export declare const MapCDDLSchema: Schema.Map$< +export declare const MapCDDLSchema: Schema.MapFromSelf< Schema.Tuple2, Schema.Tuple2< Schema.Schema, @@ -513,3 +674,30 @@ export declare const MapCDDLSchema: Schema.Map$< ``` Added in v2.0.0 + +## Redeemers + +Union schema for redeemers — accepts either map or array format. +Follows the Credential pattern: `Credential = Union(KeyHash, ScriptHash)`. + +**Signature** + +```ts +export declare const Redeemers: Schema.Union<[typeof RedeemerMap, typeof RedeemerArray]> +``` + +Added in v2.0.0 + +# utilities + +## keyToString + +Create a string key from a RedeemerKey for lookup convenience. + +**Signature** + +```ts +export declare const keyToString: ([tag, index]: RedeemerKey) => string +``` + +Added in v2.0.0 diff --git a/docs/content/docs/modules/TSchema.mdx b/docs/content/docs/modules/TSchema.mdx index 99a264bb..422bbacf 100644 --- a/docs/content/docs/modules/TSchema.mdx +++ b/docs/content/docs/modules/TSchema.mdx @@ -18,6 +18,7 @@ parent: Modules - [schemas](#schemas) - [ByteArray](#bytearray) - [Integer](#integer) + - [PlutusData](#plutusdata) - [utils](#utils) - [Array](#array) - [Array (interface)](#array-interface) @@ -32,6 +33,7 @@ parent: Modules - [Map (interface)](#map-interface) - [NullOr](#nullor) - [NullOr (interface)](#nullor-interface) + - [PlutusData (interface)](#plutusdata-interface) - [Struct](#struct) - [Struct (interface)](#struct-interface) - [StructOptions (interface)](#structoptions-interface) @@ -133,6 +135,21 @@ export declare const Integer: Integer Added in v2.0.0 +## PlutusData + +Opaque PlutusData schema for use inside TSchema combinators. +Represents an arbitrary PlutusData value that passes through encoding unchanged. + +Use this when a field can hold any PlutusData without a specific schema. + +**Signature** + +```ts +export declare const PlutusData: PlutusData +``` + +Added in v2.0.0 + # utils ## Array @@ -298,6 +315,14 @@ export interface NullOr extends Schema.transform, Schema.NullOr> {} ``` +## PlutusData (interface) + +**Signature** + +```ts +export interface PlutusData extends Schema.Schema {} +``` + ## Struct Creates a schema for struct types using Plutus Data Constructor diff --git a/docs/content/docs/modules/Transaction.mdx b/docs/content/docs/modules/Transaction.mdx index dee0f686..f595f3fd 100644 --- a/docs/content/docs/modules/Transaction.mdx +++ b/docs/content/docs/modules/Transaction.mdx @@ -10,6 +10,13 @@ parent: Modules

Table of contents

+- [encoding](#encoding) + - [addVKeyWitnesses](#addvkeywitnesses) + - [addVKeyWitnessesBytes](#addvkeywitnessesbytes) + - [addVKeyWitnessesHex](#addvkeywitnesseshex) + - [extractBodyBytes](#extractbodybytes) + - [toCBORBytesWithFormat](#tocborbyteswithformat) + - [toCBORHexWithFormat](#tocborhexwithformat) - [model](#model) - [Transaction (class)](#transaction-class) - [toJSON (method)](#tojson-method) @@ -17,6 +24,9 @@ parent: Modules - [[Inspectable.NodeInspectSymbol] (method)](#inspectablenodeinspectsymbol-method) - [[Equal.symbol] (method)](#equalsymbol-method) - [[Hash.symbol] (method)](#hashsymbol-method) +- [parsing](#parsing) + - [fromCBORBytesWithFormat](#fromcborbyteswithformat) + - [fromCBORHexWithFormat](#fromcborhexwithformat) - [utils](#utils) - [CDDLSchema](#cddlschema) - [FromCBORBytes](#fromcborbytes) @@ -30,6 +40,102 @@ parent: Modules --- +# encoding + +## addVKeyWitnesses + +Add VKey witnesses to a transaction at the domain level. + +This creates a new Transaction with the additional witnesses merged in. +All encoding metadata (body bytes, redeemers format, witness map structure) +is preserved so that txId and scriptDataHash remain stable. + +**Signature** + +```ts +export declare const addVKeyWitnesses: ( + tx: Transaction, + witnesses: ReadonlyArray +) => Transaction +``` + +Added in v2.0.0 + +## addVKeyWitnessesBytes + +Merge wallet vkey witnesses into a transaction at the raw CBOR byte level. + +Works like CML: the entire transaction byte stream is preserved except for +the vkey witnesses value in the witness set map. Body, redeemers, datums, +scripts, isValid, auxiliaryData, and map entry ordering stay byte-for-byte +identical — preserving both the txId and scriptDataHash. + +**Signature** + +```ts +export declare const addVKeyWitnessesBytes: ( + txBytes: Uint8Array, + walletWitnessSetBytes: Uint8Array, + options?: CBOR.CodecOptions +) => Uint8Array +``` + +Added in v2.0.0 + +## addVKeyWitnessesHex + +Hex variant of `addVKeyWitnessesBytes`. + +**Signature** + +```ts +export declare const addVKeyWitnessesHex: ( + txHex: string, + walletWitnessSetHex: string, + options?: CBOR.CodecOptions +) => string +``` + +Added in v2.0.0 + +## extractBodyBytes + +Extract the original body bytes from a raw transaction CBOR byte array. +A Cardano transaction is a 4-element CBOR array: `[body, witnessSet, isValid, auxiliaryData]`. +This returns the raw body bytes without decoding/re-encoding, preserving the exact CBOR encoding. + +**Signature** + +```ts +export declare const extractBodyBytes: (txBytes: Uint8Array) => Uint8Array +``` + +Added in v2.0.0 + +## toCBORBytesWithFormat + +Convert a Transaction to CBOR bytes using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORBytesWithFormat: (data: Transaction, format: CBOR.CBORFormat) => Uint8Array +``` + +Added in v2.0.0 + +## toCBORHexWithFormat + +Convert a Transaction to CBOR hex string using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORHexWithFormat: (data: Transaction, format: CBOR.CBORFormat) => string +``` + +Added in v2.0.0 + # model ## Transaction (class) @@ -87,6 +193,32 @@ toString(): string [Hash.symbol](): number ``` +# parsing + +## fromCBORBytesWithFormat + +Parse a Transaction from CBOR bytes and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORBytesWithFormat: (bytes: Uint8Array) => CBOR.DecodedWithFormat +``` + +Added in v2.0.0 + +## fromCBORHexWithFormat + +Parse a Transaction from CBOR hex string and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORHexWithFormat: (hex: string) => CBOR.DecodedWithFormat +``` + +Added in v2.0.0 + # utils ## CDDLSchema @@ -98,13 +230,11 @@ CDDL: transaction = [transaction_body, transaction_witness_set, bool, auxiliary_ **Signature** ```ts -export declare const CDDLSchema: Schema.Tuple< - [ - Schema.MapFromSelf>, - Schema.MapFromSelf>, - typeof Schema.Boolean, - Schema.Schema - ] +export declare const CDDLSchema: Schema.declare< + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [], + never > ``` @@ -124,13 +254,11 @@ export declare const FromCBORBytes: ( never >, Schema.transformOrFail< - Schema.Tuple< - [ - Schema.MapFromSelf>, - Schema.MapFromSelf>, - typeof Schema.Boolean, - Schema.Schema - ] + Schema.declare< + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [], + never >, Schema.SchemaClass, never @@ -157,13 +285,11 @@ export declare const FromCBORHex: ( > >, Schema.transformOrFail< - Schema.Tuple< - [ - Schema.MapFromSelf>, - Schema.MapFromSelf>, - typeof Schema.Boolean, - Schema.Schema - ] + Schema.declare< + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [], + never >, Schema.SchemaClass, never @@ -179,13 +305,11 @@ Transform between CDDL tuple and Transaction class. ```ts export declare const FromCDDL: Schema.transformOrFail< - Schema.Tuple< - [ - Schema.MapFromSelf>, - Schema.MapFromSelf>, - typeof Schema.Boolean, - Schema.Schema - ] + Schema.declare< + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [], + never >, Schema.SchemaClass, never diff --git a/docs/content/docs/modules/TransactionBody.mdx b/docs/content/docs/modules/TransactionBody.mdx index 22b89d45..66b7ba0b 100644 --- a/docs/content/docs/modules/TransactionBody.mdx +++ b/docs/content/docs/modules/TransactionBody.mdx @@ -14,9 +14,13 @@ parent: Modules - [arbitrary](#arbitrary-1) - [conversion](#conversion) - [fromCBORBytes](#fromcborbytes) + - [fromCBORBytesWithFormat](#fromcborbyteswithformat) - [fromCBORHex](#fromcborhex) + - [fromCBORHexWithFormat](#fromcborhexwithformat) - [toCBORBytes](#tocborbytes) + - [toCBORBytesWithFormat](#tocborbyteswithformat) - [toCBORHex](#tocborhex) + - [toCBORHexWithFormat](#tocborhexwithformat) - [model](#model) - [TransactionBody (class)](#transactionbody-class) - [toJSON (method)](#tojson-method) @@ -66,6 +70,18 @@ export declare const fromCBORBytes: (bytes: Uint8Array, options?: CBOR.CodecOpti Added in v2.0.0 +## fromCBORBytesWithFormat + +Parse a TransactionBody from CBOR bytes and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORBytesWithFormat: (bytes: Uint8Array) => CBOR.DecodedWithFormat +``` + +Added in v2.0.0 + ## fromCBORHex Convert CBOR hex string to TransactionBody. @@ -78,6 +94,18 @@ export declare const fromCBORHex: (hex: string, options?: CBOR.CodecOptions) => Added in v2.0.0 +## fromCBORHexWithFormat + +Parse a TransactionBody from CBOR hex string and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORHexWithFormat: (hex: string) => CBOR.DecodedWithFormat +``` + +Added in v2.0.0 + ## toCBORBytes Convert TransactionBody to CBOR bytes. @@ -90,6 +118,18 @@ export declare const toCBORBytes: (data: TransactionBody, options?: CBOR.CodecOp Added in v2.0.0 +## toCBORBytesWithFormat + +Convert a TransactionBody to CBOR bytes using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORBytesWithFormat: (data: TransactionBody, format: CBOR.CBORFormat) => Uint8Array +``` + +Added in v2.0.0 + ## toCBORHex Convert TransactionBody to CBOR hex string. @@ -102,6 +142,18 @@ export declare const toCBORHex: (data: TransactionBody, options?: CBOR.CodecOpti Added in v2.0.0 +## toCBORHexWithFormat + +Convert a TransactionBody to CBOR hex string using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORHexWithFormat: (data: TransactionBody, format: CBOR.CBORFormat) => string +``` + +Added in v2.0.0 + # model ## TransactionBody (class) @@ -195,10 +247,7 @@ CDDL schema for TransactionBody struct structure. **Signature** ```ts -export declare const CDDLSchema: Schema.MapFromSelf< - typeof Schema.BigIntFromSelf, - Schema.Schema -> +export declare const CDDLSchema: Schema.declare, Map, readonly [], never> ``` Added in v2.0.0 @@ -220,7 +269,7 @@ export declare const FromCBORBytes: ( never >, Schema.transformOrFail< - Schema.MapFromSelf>, + Schema.declare, Map, readonly [], never>, Schema.SchemaClass, never > @@ -249,7 +298,7 @@ export declare const FromCBORHex: ( > >, Schema.transformOrFail< - Schema.MapFromSelf>, + Schema.declare, Map, readonly [], never>, Schema.SchemaClass, never > @@ -266,7 +315,7 @@ Added in v2.0.0 ```ts export declare const FromCDDL: Schema.transformOrFail< - Schema.MapFromSelf>, + Schema.declare, Map, readonly [], never>, Schema.SchemaClass, never > diff --git a/docs/content/docs/modules/TransactionMetadatum.mdx b/docs/content/docs/modules/TransactionMetadatum.mdx index 673bea81..ea3acc42 100644 --- a/docs/content/docs/modules/TransactionMetadatum.mdx +++ b/docs/content/docs/modules/TransactionMetadatum.mdx @@ -20,6 +20,8 @@ parent: Modules - [encoding](#encoding) - [toCBORBytes](#tocborbytes) - [toCBORHex](#tocborhex) +- [equality](#equality) + - [equals](#equals) - [model](#model) - [List (type alias)](#list-type-alias) - [Map (type alias)](#map-type-alias) @@ -37,8 +39,6 @@ parent: Modules - [MapSchema](#mapschema) - [TextSchema](#textschema) - [TransactionMetadatumSchema](#transactionmetadatumschema) -- [utilities](#utilities) - - [equals](#equals) - [utils](#utils) - [arbitrary](#arbitrary) @@ -144,6 +144,22 @@ export declare const toCBORHex: (data: TransactionMetadatum, options?: CBOR.Code Added in v2.0.0 +# equality + +## equals + +Schema-derived structural equality for TransactionMetadatum values. +Handles maps, lists, ints, bytes, and text via the +recursive TransactionMetadatumSchema definition — no hand-rolled comparison needed. + +**Signature** + +```ts +export declare const equals: (a: TransactionMetadatum, b: TransactionMetadatum) => boolean +``` + +Added in v2.0.0 + # model ## List (type alias) @@ -436,20 +452,6 @@ export declare const TransactionMetadatumSchema: Schema.Union< Added in v2.0.0 -# utilities - -## equals - -Check if two TransactionMetadatum instances are equal. - -**Signature** - -```ts -export declare const equals: (a: TransactionMetadatum, b: TransactionMetadatum) => boolean -``` - -Added in v2.0.0 - # utils ## arbitrary diff --git a/docs/content/docs/modules/TransactionWitnessSet.mdx b/docs/content/docs/modules/TransactionWitnessSet.mdx index c310cf0f..6431d15c 100644 --- a/docs/content/docs/modules/TransactionWitnessSet.mdx +++ b/docs/content/docs/modules/TransactionWitnessSet.mdx @@ -18,7 +18,9 @@ parent: Modules - [fromVKeyWitnesses](#fromvkeywitnesses) - [encoding](#encoding) - [toCBORBytes](#tocborbytes) + - [toCBORBytesWithFormat](#tocborbyteswithformat) - [toCBORHex](#tocborhex) + - [toCBORHexWithFormat](#tocborhexwithformat) - [model](#model) - [PlutusScript](#plutusscript) - [TransactionWitnessSet (class)](#transactionwitnessset-class) @@ -35,7 +37,9 @@ parent: Modules - [[Hash.symbol] (method)](#hashsymbol-method-1) - [parsing](#parsing) - [fromCBORBytes](#fromcborbytes) + - [fromCBORBytesWithFormat](#fromcborbyteswithformat) - [fromCBORHex](#fromcborhex) + - [fromCBORHexWithFormat](#fromcborhexwithformat) - [schemas](#schemas) - [CDDLSchema](#cddlschema) - [FromCDDL](#fromcddl) @@ -112,6 +116,18 @@ export declare const toCBORBytes: (data: TransactionWitnessSet, options?: CBOR.C Added in v2.0.0 +## toCBORBytesWithFormat + +Convert a TransactionWitnessSet to CBOR bytes using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORBytesWithFormat: (data: TransactionWitnessSet, format: CBOR.CBORFormat) => Uint8Array +``` + +Added in v2.0.0 + ## toCBORHex Convert a TransactionWitnessSet to CBOR hex string. @@ -124,6 +140,18 @@ export declare const toCBORHex: (data: TransactionWitnessSet, options?: CBOR.Cod Added in v2.0.0 +## toCBORHexWithFormat + +Convert a TransactionWitnessSet to CBOR hex string using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORHexWithFormat: (data: TransactionWitnessSet, format: CBOR.CBORFormat) => string +``` + +Added in v2.0.0 + # model ## PlutusScript @@ -302,6 +330,18 @@ export declare const fromCBORBytes: (bytes: Uint8Array, options?: CBOR.CodecOpti Added in v2.0.0 +## fromCBORBytesWithFormat + +Parse a TransactionWitnessSet from CBOR bytes and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORBytesWithFormat: (bytes: Uint8Array) => CBOR.DecodedWithFormat +``` + +Added in v2.0.0 + ## fromCBORHex Parse a TransactionWitnessSet from CBOR hex string. @@ -314,6 +354,18 @@ export declare const fromCBORHex: (hex: string, options?: CBOR.CodecOptions) => Added in v2.0.0 +## fromCBORHexWithFormat + +Parse a TransactionWitnessSet from CBOR hex string and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORHexWithFormat: (hex: string) => CBOR.DecodedWithFormat +``` + +Added in v2.0.0 + # schemas ## CDDLSchema @@ -337,10 +389,7 @@ nonempty_set = #6.258([+ a0]) / [+ a0] **Signature** ```ts -export declare const CDDLSchema: Schema.MapFromSelf< - typeof Schema.BigIntFromSelf, - Schema.Schema -> +export declare const CDDLSchema: Schema.declare, Map, readonly [], never> ``` Added in v2.0.0 @@ -353,7 +402,7 @@ CDDL transformation schema for TransactionWitnessSet. ```ts export declare const FromCDDL: Schema.transformOrFail< - Schema.MapFromSelf>, + Schema.declare, Map, readonly [], never>, Schema.SchemaClass, never > @@ -377,7 +426,7 @@ export declare const FromCBORBytes: ( never >, Schema.transformOrFail< - Schema.MapFromSelf>, + Schema.declare, Map, readonly [], never>, Schema.SchemaClass, never > @@ -401,7 +450,7 @@ export declare const FromCBORHex: ( > >, Schema.transformOrFail< - Schema.MapFromSelf>, + Schema.declare, Map, readonly [], never>, Schema.SchemaClass, never > diff --git a/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx b/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx index e3b210e5..6f5a0e63 100644 --- a/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx +++ b/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx @@ -984,6 +984,9 @@ export interface ProtocolParameters { /** Price per CPU step for script execution (optional, for ExUnits cost calculation) */ priceStep?: number + /** Cost per byte for reference scripts (Conway-era, default 44) */ + minFeeRefScriptCostPerByte?: number + // Future fields for advanced features: // maxBlockHeaderSize?: number // maxTxExecutionUnits?: ExUnits @@ -1735,12 +1738,9 @@ export interface BuildOptions { /** * Format for encoding redeemers in the script data hash. * - * - `"array"` (DEFAULT): Conway-era format, redeemers encoded as array - * - `"map"`: Babbage-era format, redeemers encoded as map - * - * Use `"map"` for Babbage compatibility or debugging. + * @deprecated Redeemer format is now determined by the concrete `Redeemers` type + * (`RedeemerMap` or `RedeemerArray`). This option is ignored. * - * @default "array" * @since 2.0.0 */ readonly scriptDataFormat?: "array" | "map" diff --git a/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx b/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx index cb3f8889..3f1063e8 100644 --- a/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx +++ b/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx @@ -33,6 +33,7 @@ parent: Modules - [makeTxOutput](#maketxoutput) - [mergeAssetsIntoOutput](#mergeassetsintooutput) - [mergeAssetsIntoUTxO](#mergeassetsintoutxo) + - [tierRefScriptFee](#tierrefscriptfee) - [validation](#validation) - [calculateLeftoverAssets](#calculateleftoverassets) - [validateTransactionBalance](#validatetransactionbalance) @@ -87,7 +88,11 @@ Added in v2.0.0 ## calculateMinimumUtxoLovelace Calculate minimum ADA required for a UTxO based on its actual CBOR size. -Uses the Babbage-era formula: coinsPerUtxoByte \* utxoSize. +Uses the Babbage/Conway-era formula: coinsPerUtxoByte \* (160 + serializedOutputSize). + +The 160-byte constant accounts for the UTxO entry overhead in the ledger state +(transaction hash + index). A lovelace placeholder is used during CBOR encoding +to ensure the coin field width matches the final result. This function creates a temporary TransactionOutput, encodes it to CBOR, and calculates the exact size to determine the minimum lovelace required. @@ -248,18 +253,24 @@ Added in v2.0.0 Calculate reference script fees using tiered pricing. -Reference scripts stored on-chain incur additional fees based on their size: +Matches the Cardano node's `tierRefScriptFee` from Conway ledger: + +- Stride: 25,600 bytes (hardcoded, becomes a protocol param post-Conway) +- Multiplier: 1.2× per tier (hardcoded, becomes a protocol param post-Conway) +- Base cost: `minFeeRefScriptCostPerByte` protocol parameter -- First 25KB: 15 lovelace/byte -- Next 25KB: 25 lovelace/byte -- Next 150KB: 100 lovelace/byte -- Maximum: 200KB total +For each 25,600-byte chunk the price per byte increases by 1.2×. +The final (partial) chunk is charged proportionally. Result is `floor(total)`. + +The Cardano node sums scriptRef sizes from both spent inputs and reference +inputs (`txNonDistinctRefScriptsSize`), so callers must pass both. **Signature** ```ts export declare const calculateReferenceScriptFee: ( - referenceInputs: ReadonlyArray + utxos: ReadonlyArray, + costPerByte: number ) => Effect.Effect ``` @@ -375,6 +386,27 @@ export declare const mergeAssetsIntoUTxO: ( Added in v2.0.0 +## tierRefScriptFee + +Calculate reference script fees using tiered pricing. + +Direct port of the Cardano ledger's `tierRefScriptFee` function. +Each `sizeIncrement`-byte chunk is priced at `curTierPrice` per byte, +then `curTierPrice *= multiplier` for the next chunk. Final result: `floor(total)`. + +**Signature** + +```ts +export declare const tierRefScriptFee: ( + multiplier: number, + sizeIncrement: number, + baseFee: number, + totalSize: number +) => bigint +``` + +Added in v2.0.0 + # validation ## calculateLeftoverAssets diff --git a/docs/content/docs/modules/utils/Hash.mdx b/docs/content/docs/modules/utils/Hash.mdx index b49b75a0..8c6a6c88 100644 --- a/docs/content/docs/modules/utils/Hash.mdx +++ b/docs/content/docs/modules/utils/Hash.mdx @@ -11,29 +11,16 @@ parent: Modules

Table of contents

- [utils](#utils) - - [RedeemersFormat (type alias)](#redeemersformat-type-alias) - [computeTotalExUnits](#computetotalexunits) - [hashAuxiliaryData](#hashauxiliarydata) - [hashScriptData](#hashscriptdata) - [hashTransaction](#hashtransaction) + - [hashTransactionRaw](#hashtransactionraw) --- # utils -## RedeemersFormat (type alias) - -Format for encoding redeemers in the script data hash. - -- "array": Legacy format `[ + redeemer ]` (Shelley-Babbage) -- "map": Conway format `{ + [tag, index] => [data, ex_units] }` - -**Signature** - -```ts -export type RedeemersFormat = "array" | "map" -``` - ## computeTotalExUnits Compute total ex_units by summing over redeemers. @@ -58,6 +45,9 @@ export declare const hashAuxiliaryData: (aux: AuxiliaryData.AuxiliaryData) => Au Compute script_data_hash using standard module encoders. +Accepts the concrete `Redeemers` union type — encoding format is determined +by `_tag` (`RedeemerMap` → map CBOR, `RedeemerArray` → array CBOR). + The payload format per CDDL spec is raw concatenation (not a CBOR structure): ``` @@ -68,10 +58,9 @@ redeemers_bytes || datums_bytes || language_views_bytes ```ts export declare const hashScriptData: ( - redeemers: ReadonlyArray, + redeemers: Redeemers.Redeemers, costModels: CostModel.CostModels, datums?: ReadonlyArray, - format?: RedeemersFormat, options?: CBOR.CodecOptions ) => ScriptDataHash.ScriptDataHash ``` @@ -85,3 +74,14 @@ Compute the transaction body hash (blake2b-256 over CBOR of body). ```ts export declare const hashTransaction: (body: TransactionBody.TransactionBody) => TransactionHash.TransactionHash ``` + +## hashTransactionRaw + +Compute the transaction body hash from raw CBOR bytes, preserving original encoding. +Uses `Transaction.extractBodyBytes` to avoid the decode→re-encode round-trip. + +**Signature** + +```ts +export declare const hashTransactionRaw: (bodyBytes: Uint8Array) => TransactionHash.TransactionHash +``` diff --git a/packages/evolution-mcp/README.md b/packages/evolution-mcp/README.md new file mode 100644 index 00000000..cf71429e --- /dev/null +++ b/packages/evolution-mcp/README.md @@ -0,0 +1,56 @@ +# Evolution MCP + +`@evolution-sdk/mcp` exposes Evolution SDK functionality as an MCP server with both HTTP and stdio transports. + +Default runtime configuration (HTTP mode): + +- Host: `127.0.0.1` +- Port: `10000` +- MCP endpoint: `/mcp` +- Health endpoint: `/health` + +The package ships a Linux-first `postinstall` bootstrap that attempts to register and start a local background process so the server is reachable at `http://localhost:10000/mcp` after installation. If automatic startup cannot be completed, installation stays successful and the package prints a manual fallback. + +## Commands + +```bash +pnpm --filter @evolution-sdk/mcp build +pnpm --filter @evolution-sdk/mcp test + +# HTTP server (default) +node packages/evolution-mcp/dist/bin.js serve + +# Stdio transport (for MCP clients that spawn the process) +node packages/evolution-mcp/dist/bin.js stdio +``` + +## Environment Variables + +- `EVOLUTION_MCP_HOST`: bind host, default `127.0.0.1` +- `EVOLUTION_MCP_PORT`: bind port, default `10000` +- `EVOLUTION_MCP_PATH`: MCP route, default `/mcp` +- `EVOLUTION_MCP_HEALTH_PATH`: health route, default `/health` +- `EVOLUTION_MCP_SKIP_POSTINSTALL`: skip install-time bootstrap when set to `1` +- `EVOLUTION_MCP_POSTINSTALL_STRICT`: fail install if bootstrap fails when set to `1` + +## Tool Surface (66 tools) + +| Category | Count | Tools | +|----------|-------|-------| +| Meta / Introspection | 3 | `sdk_info`, `sdk_exports`, `destroy_handle` | +| Codecs & Encoding | 8 | `address_codec`, `assets_codec`, `cbor_codec`, `data_codec`, `identifier_codec`, `typed_export_codec`, `encoding_codec`, `plutus_data_codec_tools` | +| Workflow | 7 | `create_client`, `client_attach`, `client_invoke`, `tx_builder_create`, `tx_builder_apply`, `tx_builder_build`, `result_call` | +| Cryptography | 5 | `key_generate`, `bip32_key_tools`, `message_sign`, `message_verify`, `ed25519_signature_tools` | +| Governance & Certificates | 10 | `anchor_tools`, `certificate_tools`, `voting_tools`, `governance_action_tools`, `proposal_tools`, `drep_tools`, `drep_cert_tools`, `committee_cert_tools`, `constitution_tools`, `protocol_param_update_tools` | +| Transaction Primitives | 9 | `transaction_input_tools`, `transaction_body_tools`, `tx_output_tools`, `mint_tools`, `withdrawals_tools`, `redeemer_tools`, `redeemers_collection_tools`, `proposal_procedures_collection_tools`, `script_ref_tools` | +| Value & Assets | 5 | `value_tools`, `assets_tools`, `unit_tools`, `coin_tools`, `plutus_value_tools` | +| Scripts | 4 | `native_script_tools`, `script_tools`, `uplc_tools`, `evaluator_info` | +| Address Types | 3 | `address_build`, `pointer_address_tools`, `byron_address_tools` | +| Data & Hashing | 2 | `data_construct`, `hash_tools` | +| Blueprints | 2 | `blueprint_parse`, `blueprint_codegen` | +| Network & Time | 2 | `network_tools`, `time_slot_convert` | +| Metadata & Credentials | 2 | `metadata_tools`, `credential_tools` | +| Other | 3 | `utxo_tools`, `pool_params_tools`, `fee_validate` | +| Devnet | 1 | `devnet` | + +This package covers all four workspace packages: `@evolution-sdk/evolution`, `@evolution-sdk/aiken-uplc`, `@evolution-sdk/scalus-uplc`, and `@evolution-sdk/devnet`. \ No newline at end of file diff --git a/packages/evolution-mcp/package.json b/packages/evolution-mcp/package.json new file mode 100644 index 00000000..3680203b --- /dev/null +++ b/packages/evolution-mcp/package.json @@ -0,0 +1,89 @@ +{ + "name": "@evolution-sdk/mcp", + "version": "0.1.0", + "description": "HTTP MCP server for Evolution SDK", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "evolution-mcp": "./dist/bin.js" + }, + "sideEffects": [], + "tags": [ + "typescript", + "mcp", + "http", + "cardano", + "sdk" + ], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "scripts/**/*.mjs", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map", + "README.md" + ], + "scripts": { + "build": "tsc -b tsconfig.build.json", + "dev": "tsc -b tsconfig.build.json --watch", + "type-check": "tsc --noEmit", + "lint": "eslint \"src/**/*.{ts,mjs}\" \"test/**/*.{ts,mjs}\"", + "test": "vitest run", + "clean": "rm -rf dist .turbo .tsbuildinfo", + "postinstall": "node ./scripts/postinstall.mjs" + }, + "dependencies": { + "@effect/platform": "^0.90.10", + "@effect/platform-node": "^0.96.1", + "@evolution-sdk/aiken-uplc": "workspace:*", + "@evolution-sdk/devnet": "workspace:*", + "@evolution-sdk/evolution": "workspace:*", + "@evolution-sdk/scalus-uplc": "workspace:*", + "@modelcontextprotocol/sdk": "^1.27.1", + "effect": "^3.19.3", + "zod": "^4.1.11" + }, + "devDependencies": { + "tsx": "^4.20.4", + "typescript": "^5.9.2" + }, + "keywords": [ + "mcp", + "model-context-protocol", + "cardano", + "evolution", + "sdk", + "http" + ], + "homepage": "https://github.com/IntersectMBO/evolution-sdk", + "repository": { + "type": "git", + "url": "git+https://github.com/IntersectMBO/evolution-sdk.git", + "directory": "packages/evolution-mcp" + }, + "bugs": { + "url": "https://github.com/IntersectMBO/evolution-sdk/issues" + }, + "license": "MIT", + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./*/index": null + } + } +} \ No newline at end of file diff --git a/packages/evolution-mcp/scripts/postinstall.mjs b/packages/evolution-mcp/scripts/postinstall.mjs new file mode 100644 index 00000000..d8f920bd --- /dev/null +++ b/packages/evolution-mcp/scripts/postinstall.mjs @@ -0,0 +1,250 @@ +import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises" +import { constants } from "node:fs" +import { createConnection } from "node:net" +import { dirname, join } from "node:path" +import { homedir } from "node:os" +import { spawn } from "node:child_process" +import { fileURLToPath } from "node:url" + +const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))) +const distBin = join(packageRoot, "dist", "bin.js") +const serviceName = process.env.EVOLUTION_MCP_SERVICE_NAME ?? "evolution-mcp" +const host = process.env.EVOLUTION_MCP_HOST ?? "127.0.0.1" +const port = Number.parseInt(process.env.EVOLUTION_MCP_PORT ?? "10000", 10) +const strict = process.env.EVOLUTION_MCP_POSTINSTALL_STRICT === "1" +const shouldSkip = + process.env.EVOLUTION_MCP_SKIP_POSTINSTALL === "1" || + process.env.CI === "true" || + process.env.npm_config_global === "true" + +const xdgConfigHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config") +const xdgStateHome = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state") +const systemdDir = join(xdgConfigHome, "systemd", "user") +const stateDir = join(xdgStateHome, serviceName) +const logFile = join(stateDir, "server.log") +const pidFile = join(stateDir, "server.pid") +const serviceFile = join(systemdDir, `${serviceName}.service`) + +const info = (message) => process.stdout.write(`[${serviceName}] ${message}\n`) +const warn = (message) => process.stderr.write(`[${serviceName}] ${message}\n`) + +const failOrWarn = (message) => { + if (strict) { + throw new Error(message) + } + + warn(message) +} + +const pathExists = async (target) => { + try { + await access(target, constants.F_OK) + return true + } catch { + return false + } +} + +const commandAvailable = async (command) => + new Promise((resolve) => { + const child = spawn("sh", ["-lc", `command -v ${command}`], { stdio: "ignore" }) + child.on("close", (code) => resolve(code === 0)) + child.on("error", () => resolve(false)) + }) + +const canUseSystemdUser = async () => { + if (!(await commandAvailable("systemctl"))) { + return false + } + + return new Promise((resolve) => { + let output = "" + const child = spawn("systemctl", ["--user", "show-environment"], { stdio: ["ignore", "pipe", "pipe"] }) + + child.stdout.on("data", (chunk) => { + output += chunk.toString() + }) + + child.stderr.on("data", (chunk) => { + output += chunk.toString() + }) + + child.on("close", (code) => { + const normalizedOutput = output.toLowerCase() + const unavailable = normalizedOutput.includes("systemd") && normalizedOutput.includes("not running") + resolve(code === 0 && !unavailable) + }) + child.on("error", () => resolve(false)) + }) +} + +const isPortReachable = async () => + new Promise((resolve) => { + const socket = createConnection({ host, port }) + socket.once("connect", () => { + socket.end() + resolve(true) + }) + socket.once("error", () => resolve(false)) + socket.setTimeout(1000, () => { + socket.destroy() + resolve(false) + }) + }) + +const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)) + +const waitForReachable = async ({ timeoutMs = 15000, intervalMs = 250 } = {}) => { + const deadline = Date.now() + timeoutMs + + while (Date.now() < deadline) { + if (await isPortReachable()) { + return true + } + + await sleep(intervalMs) + } + + return isPortReachable() +} + +const startDetached = async () => { + await mkdir(stateDir, { recursive: true }) + + const launchCommand = (await commandAvailable("setsid")) ? "setsid" : process.execPath + const launchArgs = launchCommand === "setsid" ? [process.execPath, distBin, "serve"] : [distBin, "serve"] + const child = spawn(launchCommand, launchArgs, { + cwd: packageRoot, + detached: launchCommand !== "setsid", + stdio: "ignore", + env: { + ...process.env, + EVOLUTION_MCP_HOST: host, + EVOLUTION_MCP_PORT: String(port) + } + }) + + child.unref() + await writeFile(pidFile, `${child.pid ?? ""}\n`, "utf8") +} + +const cleanupStalePid = async () => { + if (!(await pathExists(pidFile))) { + return + } + + const pid = Number.parseInt((await readFile(pidFile, "utf8")).trim(), 10) + if (!Number.isInteger(pid)) { + await rm(pidFile, { force: true }) + return + } + + try { + process.kill(pid, 0) + } catch { + await rm(pidFile, { force: true }) + } +} + +const installSystemdService = async () => { + await mkdir(systemdDir, { recursive: true }) + await mkdir(stateDir, { recursive: true }) + + const unit = [ + "[Unit]", + "Description=Evolution MCP HTTP server", + "After=default.target", + "", + "[Service]", + "Type=simple", + `WorkingDirectory=${packageRoot}`, + `Environment=EVOLUTION_MCP_HOST=${host}`, + `Environment=EVOLUTION_MCP_PORT=${port}`, + `ExecStart=${process.execPath} ${distBin} serve`, + `StandardOutput=append:${logFile}`, + `StandardError=append:${logFile}`, + "Restart=on-failure", + "RestartSec=2", + "", + "[Install]", + "WantedBy=default.target", + "" + ].join("\n") + + await writeFile(serviceFile, unit, "utf8") + + const run = (args) => + new Promise((resolve, reject) => { + const child = spawn("systemctl", ["--user", ...args], { stdio: "ignore" }) + child.on("close", (code) => { + if (code === 0) { + resolve(undefined) + return + } + reject(new Error(`systemctl --user ${args.join(" ")} exited with code ${code}`)) + }) + child.on("error", reject) + }) + + await run(["daemon-reload"]) + await run(["enable", "--now", `${serviceName}.service`]) +} + +const main = async () => { + if (shouldSkip) { + info("Skipping postinstall bootstrap") + return + } + + if (process.platform !== "linux") { + failOrWarn( + "Automatic postinstall bootstrap is only implemented for Linux. On macOS and Windows, the server will not start automatically; start it manually with: node dist/bin.js serve" + ) + return + } + + if (!(await pathExists(distBin))) { + failOrWarn("Built server entrypoint not found. Build the package, then start manually with: node dist/bin.js serve") + return + } + + if (await isPortReachable()) { + info(`Port ${port} is already serving. Skipping bootstrap.`) + return + } + + await cleanupStalePid() + + try { + if (await canUseSystemdUser()) { + await installSystemdService() + } else { + await startDetached() + } + } catch (error) { + warn(`Automatic bootstrap failed: ${error instanceof Error ? error.message : String(error)}`) + try { + await startDetached() + } catch (fallbackError) { + failOrWarn( + `Fallback bootstrap failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}. Start manually with: node dist/bin.js serve` + ) + return + } + } + + const becameReachable = await waitForReachable() + if (becameReachable) { + info(`MCP server is available at http://${host}:${port}/mcp`) + return + } + + failOrWarn(`Bootstrap completed but the server is not reachable yet. Check ${logFile} or run: node dist/bin.js serve`) +} + +void main().catch((error) => { + if (strict) { + throw error + } + warn(error instanceof Error ? error.message : String(error)) +}) \ No newline at end of file diff --git a/packages/evolution-mcp/src/bin.ts b/packages/evolution-mcp/src/bin.ts new file mode 100644 index 00000000..124562b7 --- /dev/null +++ b/packages/evolution-mcp/src/bin.ts @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +import { once } from "node:events" + +import { resolveConfig } from "./config.js" +import { startHttpServer } from "./http.js" +import { startStdioServer } from "./stdio.js" + +const args = process.argv.slice(2) +const command = args[0] ?? "serve" + +const readFlag = (name: string): string | undefined => { + const index = args.indexOf(name) + if (index === -1) { + return undefined + } + + return args[index + 1] +} + +const usage = (): void => { + process.stdout.write( + [ + "Usage:", + " evolution-mcp serve [--host HOST] [--port PORT] [--path /mcp] [--health-path /health]", + " evolution-mcp stdio", + "", + "Commands:", + " serve Start HTTP server (default)", + " stdio Start JSON-RPC stdio transport (stdin/stdout)", + "", + "Defaults (serve):", + " host=127.0.0.1 port=10000 path=/mcp health-path=/health" + ].join("\n") + "\n" + ) +} + +const main = async (): Promise => { + if (command === "help" || command === "--help" || command === "-h") { + usage() + return + } + + if (command === "stdio") { + await startStdioServer() + return + } + + if (command !== "serve" && command !== "start") { + usage() + throw new Error(`Unsupported command: ${command}`) + } + + const config = resolveConfig({ + host: readFlag("--host"), + port: readFlag("--port") ? Number.parseInt(readFlag("--port") as string, 10) : undefined, + mcpPath: readFlag("--path"), + healthPath: readFlag("--health-path") + }) + + const { server } = await startHttpServer(config) + process.stdout.write(`Evolution MCP listening on http://${config.host}:${config.port}${config.mcpPath}\n`) + process.stdout.write(`Health endpoint: http://${config.host}:${config.port}${config.healthPath}\n`) + + const shutdown = async (): Promise => { + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error) + return + } + resolve() + }) + }) + } + + process.once("SIGINT", () => { + void shutdown().finally(() => process.exit(0)) + }) + process.once("SIGTERM", () => { + void shutdown().finally(() => process.exit(0)) + }) + + await once(server, "close") +} + +void main().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`) + process.exitCode = 1 +}) \ No newline at end of file diff --git a/packages/evolution-mcp/src/codec.ts b/packages/evolution-mcp/src/codec.ts new file mode 100644 index 00000000..16facd7f --- /dev/null +++ b/packages/evolution-mcp/src/codec.ts @@ -0,0 +1,108 @@ +import * as Evolution from "@evolution-sdk/evolution" + +export interface AssetRecordInput { + readonly [unit: string]: string | number +} + +export interface ProtocolParametersInput { + readonly minFeeCoefficient: string | number + readonly minFeeConstant: string | number + readonly coinsPerUtxoByte: string | number + readonly maxTxSize: number + readonly priceMem?: number + readonly priceStep?: number + readonly minFeeRefScriptCostPerByte?: number +} + +export interface UtxoInput { + readonly transactionId: string + readonly index: string | number + readonly address: string + readonly assets: AssetRecordInput + readonly datumOptionCborHex?: string + readonly scriptRefCborHex?: string +} + +const replacer = (_key: string, value: unknown): unknown => { + if (typeof value === "bigint") { + return value.toString() + } + + return value +} + +export const toStructured = (value: T): T => JSON.parse(JSON.stringify(value, replacer)) as T + +export const parseBigInt = (value: string | number): bigint => { + if (typeof value === "number") { + return BigInt(value) + } + + return BigInt(value) +} + +export const parseAddress = (value: string): Evolution.Address.Address => Evolution.Address.fromBech32(value) + +export const parseAssets = (value: AssetRecordInput): Evolution.Assets.Assets => { + const record = Object.fromEntries(Object.entries(value).map(([unit, quantity]) => [unit, parseBigInt(quantity)])) + return Evolution.Assets.fromRecord(record) +} + +export const parseProtocolParameters = ( + value: ProtocolParametersInput + ) => ({ + minFeeCoefficient: parseBigInt(value.minFeeCoefficient), + minFeeConstant: parseBigInt(value.minFeeConstant), + coinsPerUtxoByte: parseBigInt(value.coinsPerUtxoByte), + maxTxSize: value.maxTxSize, + priceMem: value.priceMem, + priceStep: value.priceStep, + minFeeRefScriptCostPerByte: value.minFeeRefScriptCostPerByte +}) + +export const parseTransaction = (cborHex: string): Evolution.Transaction.Transaction => + Evolution.Transaction.fromCBORHex(cborHex) + +export const parseWitnessSet = (cborHex: string): Evolution.TransactionWitnessSet.TransactionWitnessSet => + Evolution.TransactionWitnessSet.fromCBORHex(cborHex) + +export const parseUtxo = (value: UtxoInput): Evolution.UTxO.UTxO => + new Evolution.UTxO.UTxO({ + transactionId: Evolution.TransactionHash.fromHex(value.transactionId), + index: BigInt(value.index), + address: parseAddress(value.address), + assets: parseAssets(value.assets), + datumOption: value.datumOptionCborHex ? Evolution.DatumOption.fromCBORHex(value.datumOptionCborHex) : undefined, + scriptRef: value.scriptRefCborHex ? Evolution.Script.fromCBORHex(value.scriptRefCborHex) : undefined + }) + +export const parseUtxos = (values: ReadonlyArray): Array => values.map(parseUtxo) + +export const serializeAddress = (address: Evolution.Address.Address): string => Evolution.Address.toBech32(address) + +export const serializeTransaction = (transaction: Evolution.Transaction.Transaction) => ({ + cborHex: Evolution.Transaction.toCBORHex(transaction), + json: toStructured(transaction.toJSON()) +}) + +export const serializeWitnessSet = (witnessSet: Evolution.TransactionWitnessSet.TransactionWitnessSet) => ({ + cborHex: Evolution.TransactionWitnessSet.toCBORHex(witnessSet), + json: toStructured(witnessSet.toJSON()) +}) + +export const serializeUtxo = (utxo: Evolution.UTxO.UTxO) => ({ + outRef: Evolution.UTxO.toOutRefString(utxo), + json: toStructured(utxo.toJSON()) +}) + +export const serializeUtxos = (utxos: ReadonlyArray) => utxos.map(serializeUtxo) + +export const serializeProtocolParameters = (value: unknown) => toStructured(value) + +export const serializeDelegation = (value: { readonly poolId: unknown; readonly rewards: bigint | number }) => ({ + poolId: value.poolId === null ? null : toStructured(value.poolId), + rewards: value.rewards.toString() +}) + +export const serializeTransactionHash = (value: Evolution.TransactionHash.TransactionHash): string => + Evolution.TransactionHash.toHex(value) \ No newline at end of file diff --git a/packages/evolution-mcp/src/config.ts b/packages/evolution-mcp/src/config.ts new file mode 100644 index 00000000..96ed5579 --- /dev/null +++ b/packages/evolution-mcp/src/config.ts @@ -0,0 +1,35 @@ +export interface EvolutionMcpConfig { + readonly host: string + readonly port: number + readonly mcpPath: string + readonly healthPath: string +} + +const normalizePath = (value: string | undefined, fallback: string): string => { + const candidate = (value ?? fallback).trim() + if (candidate.length === 0) { + return fallback + } + + return candidate.startsWith("/") ? candidate : `/${candidate}` +} + +const parsePort = (value: string | undefined, fallback: number): number => { + if (!value) { + return fallback + } + + const parsed = Number.parseInt(value, 10) + if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65_535) { + return fallback + } + + return parsed +} + +export const resolveConfig = (overrides: Partial = {}): EvolutionMcpConfig => ({ + host: overrides.host ?? process.env.EVOLUTION_MCP_HOST ?? "127.0.0.1", + port: overrides.port ?? parsePort(process.env.EVOLUTION_MCP_PORT, 10_000), + mcpPath: normalizePath(overrides.mcpPath ?? process.env.EVOLUTION_MCP_PATH, "/mcp"), + healthPath: normalizePath(overrides.healthPath ?? process.env.EVOLUTION_MCP_HEALTH_PATH, "/health") +}) \ No newline at end of file diff --git a/packages/evolution-mcp/src/http.ts b/packages/evolution-mcp/src/http.ts new file mode 100644 index 00000000..3bc15037 --- /dev/null +++ b/packages/evolution-mcp/src/http.ts @@ -0,0 +1,107 @@ +import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http" + +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js" + +import { resolveConfig, type EvolutionMcpConfig } from "./config.js" +import { createEvolutionMcpServer } from "./server.js" +import { sessionStore } from "./sessions.js" + +const respondJson = (res: ServerResponse, statusCode: number, body: unknown): void => { + res.writeHead(statusCode, { + "content-type": "application/json; charset=utf-8" + }) + res.end(JSON.stringify(body)) +} + +const isRoute = (req: IncomingMessage, route: string): boolean => { + const requestUrl = req.url ?? "/" + const pathname = requestUrl.split("?")[0] + return pathname === route +} + +export interface StartedHttpServer { + readonly server: Server + readonly config: EvolutionMcpConfig +} + +export const startHttpServer = async (overrides: Partial = {}): Promise => { + const config = resolveConfig(overrides) + const mcpServer = createEvolutionMcpServer() + + // McpServer supports one transport at a time; serialize MCP requests + let pending: Promise = Promise.resolve() + const enqueue = (fn: () => Promise): Promise => { + const next = pending.then(fn, fn) + pending = next + return next + } + + const server = createServer(async (req, res) => { + if (!req.url) { + respondJson(res, 400, { error: "Missing request URL" }) + return + } + + if (isRoute(req, config.healthPath)) { + if (req.method !== "GET") { + respondJson(res, 405, { error: "Method Not Allowed", allow: ["GET"] }) + return + } + + respondJson(res, 200, { + ok: true, + host: config.host, + port: config.port, + mcpPath: config.mcpPath, + sessionStats: sessionStore.stats() + }) + return + } + + if (isRoute(req, config.mcpPath)) { + if (req.method !== "POST") { + respondJson(res, 405, { error: "Method Not Allowed", allow: ["POST"] }) + return + } + + await enqueue(async () => { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true + }) + + try { + await mcpServer.connect(transport) + await transport.handleRequest(req, res) + } catch (error) { + if (!res.headersSent) { + respondJson(res, 500, { + jsonrpc: "2.0", + error: { + code: -32_603, + message: error instanceof Error ? error.message : "Internal server error" + }, + id: null + }) + } + } finally { + await transport.close().catch(() => undefined) + } + }) + + return + } + + respondJson(res, 404, { error: "Not Found" }) + }) + + await new Promise((resolve, reject) => { + server.once("error", reject) + server.listen(config.port, config.host, () => { + server.off("error", reject) + resolve() + }) + }) + + return { server, config } +} \ No newline at end of file diff --git a/packages/evolution-mcp/src/index.ts b/packages/evolution-mcp/src/index.ts new file mode 100644 index 00000000..df6ad14d --- /dev/null +++ b/packages/evolution-mcp/src/index.ts @@ -0,0 +1,4 @@ +export { resolveConfig, type EvolutionMcpConfig } from "./config.js" +export { startHttpServer, type StartedHttpServer } from "./http.js" +export { createEvolutionMcpServer } from "./server.js" +export { startStdioServer } from "./stdio.js" \ No newline at end of file diff --git a/packages/evolution-mcp/src/server.ts b/packages/evolution-mcp/src/server.ts new file mode 100644 index 00000000..9f2d4210 --- /dev/null +++ b/packages/evolution-mcp/src/server.ts @@ -0,0 +1,5764 @@ +import type { Implementation } from "@modelcontextprotocol/sdk/types.js" +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" +import * as Evolution from "@evolution-sdk/evolution" +import * as AssetUnit from "@evolution-sdk/evolution/Assets/Unit" +import * as AssetLabel from "@evolution-sdk/evolution/Assets/Label" +import * as EvolutionHash from "@evolution-sdk/evolution/utils/Hash" +import * as Devnet from "@evolution-sdk/devnet" +import { createAikenEvaluator } from "@evolution-sdk/aiken-uplc" +import { createScalusEvaluator } from "@evolution-sdk/scalus-uplc" +import { z } from "zod" + +import { + parseAddress, + parseAssets, + parseBigInt, + parseProtocolParameters, + parseTransaction, + parseUtxos, + parseWitnessSet, + serializeAddress, + serializeDelegation, + serializeProtocolParameters, + serializeTransaction, + serializeTransactionHash, + serializeUtxos, + serializeWitnessSet, + toStructured, + type AssetRecordInput, + type ProtocolParametersInput, + type UtxoInput +} from "./codec.js" +import { sessionStore } from "./sessions.js" + +const packageVersion = "0.1.0" + +const implementation: Implementation = { + name: "@evolution-sdk/mcp", + version: packageVersion +} + +const evolutionExports = Object.keys(Evolution).sort() + +const NetworkSchema = z.union([z.enum(["mainnet", "preprod", "preview"]), z.number().int()]) + +const SlotConfigSchema = z + .object({ + zeroTime: z.union([z.string(), z.number().int()]), + zeroSlot: z.union([z.string(), z.number().int()]), + slotLength: z.number().int().positive() + }) + .optional() + +const ProviderConfigSchema = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("blockfrost"), + baseUrl: z.string().url(), + projectId: z.string().optional() + }), + z.object({ + type: z.literal("kupmios"), + kupoUrl: z.string().url(), + ogmiosUrl: z.string().url(), + headers: z + .object({ + ogmiosHeader: z.record(z.string(), z.string()).optional(), + kupoHeader: z.record(z.string(), z.string()).optional() + }) + .optional() + }), + z.object({ + type: z.literal("maestro"), + baseUrl: z.string().url(), + apiKey: z.string(), + turboSubmit: z.boolean().optional() + }), + z.object({ + type: z.literal("koios"), + baseUrl: z.string().url(), + token: z.string().optional() + }) +]) + +const WalletConfigSchema = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("seed"), + mnemonic: z.string(), + accountIndex: z.number().int().nonnegative().optional(), + paymentIndex: z.number().int().nonnegative().optional(), + stakeIndex: z.number().int().nonnegative().optional(), + addressType: z.enum(["Base", "Enterprise"]).optional(), + password: z.string().optional() + }), + z.object({ + type: z.literal("private-key"), + paymentKey: z.string(), + stakeKey: z.string().optional(), + addressType: z.enum(["Base", "Enterprise"]).optional() + }), + z.object({ + type: z.literal("read-only"), + address: z.string(), + rewardAddress: z.string().optional() + }) +]) + +const AssetRecordSchema = z.record(z.string(), z.union([z.string(), z.number().int()])) + +const UtxoInputSchema = z.object({ + transactionId: z.string(), + index: z.union([z.string(), z.number().int().nonnegative()]), + address: z.string(), + assets: AssetRecordSchema, + datumOptionCborHex: z.string().optional(), + scriptRefCborHex: z.string().optional() +}) + +const ProtocolParametersSchema = z.object({ + minFeeCoefficient: z.union([z.string(), z.number().int()]), + minFeeConstant: z.union([z.string(), z.number().int()]), + coinsPerUtxoByte: z.union([z.string(), z.number().int()]), + maxTxSize: z.number().int().positive(), + priceMem: z.number().optional(), + priceStep: z.number().optional(), + minFeeRefScriptCostPerByte: z.number().optional() +}) + +const ExportNameSchema = z.string().refine((value) => evolutionExports.includes(value), { + message: "Unknown public Evolution export" +}) + +const CborOptionsPresetSchema = z + .enum(["canonical", "cml", "cml-data", "aiken", "struct-friendly", "cardano-node-data"]) + .optional() + +const AssetDeltaSchema = z.object({ + policyIdHex: z.string(), + assetNameHex: z.string(), + quantity: z.union([z.string(), z.number().int()]) +}) + +const IdentifierKindSchema = z.enum([ + "keyHash", + "scriptHash", + "policyId", + "poolKeyHash", + "datumHash", + "transactionHash", + "credential", + "drep" +]) + +// Build a map of all Evolution modules that expose fromCBORHex / toCBORHex as data-first functions. +// DRep is excluded because its toCBORHex is curried rather than data-first. +const typedExportModules = new Map< + string, + { + fromCBORHex: (hex: string, options?: Evolution.CBOR.CodecOptions) => unknown + toCBORHex: (value: unknown, options?: Evolution.CBOR.CodecOptions) => string + } +>() + +for (const name of evolutionExports) { + if (name === "DRep") continue + const mod = (Evolution as Record)[name] + if ( + mod !== null && + typeof mod === "object" && + typeof (mod as Record).fromCBORHex === "function" && + typeof (mod as Record).toCBORHex === "function" + ) { + typedExportModules.set(name, { + fromCBORHex: (mod as Record).fromCBORHex as any, + toCBORHex: (mod as Record).toCBORHex as any + }) + } +} + +const typedExportModuleNames = Array.from(typedExportModules.keys()).sort() + +const EvaluatorSchema = z.enum(["aiken", "scalus"]).optional() + +const resolveEvaluator = (evaluator: z.infer) => { + switch (evaluator) { + case "aiken": + return createAikenEvaluator + case "scalus": + return createScalusEvaluator + default: + return undefined + } +} + +const asToolText = (value: unknown): string => JSON.stringify(toStructured(value), null, 2) + +type ToolResultObject = Record + +type StructuredCborValue = + | { readonly type: "integer"; readonly value: string } + | { readonly type: "bytes"; readonly hex: string } + | { readonly type: "text"; readonly value: string } + | { readonly type: "array"; readonly items: ReadonlyArray } + | { readonly type: "map"; readonly entries: ReadonlyArray<{ readonly key: StructuredCborValue; readonly value: StructuredCborValue }> } + | { readonly type: "record"; readonly entries: Record } + | { readonly type: "tag"; readonly tag: number; readonly value: StructuredCborValue } + | { readonly type: "boolean"; readonly value: boolean } + | { readonly type: "null" } + | { readonly type: "undefined" } + | { readonly type: "float"; readonly value: number } + | { readonly type: "boundedBytes"; readonly hex: string } + +type StructuredDataValue = + | { readonly type: "constr"; readonly index: string; readonly fields: ReadonlyArray } + | { readonly type: "map"; readonly entries: ReadonlyArray<{ readonly key: StructuredDataValue; readonly value: StructuredDataValue }> } + | { readonly type: "list"; readonly items: ReadonlyArray } + | { readonly type: "int"; readonly value: string } + | { readonly type: "bytes"; readonly hex: string } + +type StructuredIdentifier = + | { readonly type: "keyHash"; readonly hex: string } + | { readonly type: "scriptHash"; readonly hex: string } + | { readonly type: "policyId"; readonly hex: string } + | { readonly type: "poolKeyHash"; readonly hex: string; readonly bech32: string } + | { readonly type: "datumHash"; readonly hex: string } + | { readonly type: "transactionHash"; readonly hex: string } + | { readonly type: "credential"; readonly credentialType: "keyHash" | "scriptHash"; readonly hex: string; readonly cborHex: string } + | { + readonly type: "drep" + readonly drepType: "keyHash" | "scriptHash" | "alwaysAbstain" | "alwaysNoConfidence" + readonly hex?: string + readonly bech32?: string + readonly cborHex: string + } + +interface SubmitBuilderLike { + readonly witnessSet: Evolution.TransactionWitnessSet.TransactionWitnessSet + readonly submit: () => Promise +} + +const toolTextResult = (result: ToolResultObject) => ({ + content: [{ type: "text" as const, text: asToolText(result) }], + structuredContent: result +}) + +const hasMethod = (value: unknown, method: T): value is Record) => any> => + typeof value === "object" && value !== null && method in value && typeof (value as Record)[method] === "function" + +const isObject = (value: unknown): value is Record => typeof value === "object" && value !== null + +const bytesToHex = (bytes: Uint8Array): string => Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("") + +const hexToBytes = (hex: string): Uint8Array => { + if (hex.length % 2 !== 0 || /[^0-9a-f]/iu.test(hex)) { + throw new Error(`Invalid hex string: ${hex}`) + } + + const bytes = new Uint8Array(hex.length / 2) + for (let index = 0; index < hex.length; index += 2) { + bytes[index / 2] = Number.parseInt(hex.slice(index, index + 2), 16) + } + + return bytes +} + +const resolveCborOptions = ( + preset: z.infer, + fallback: Evolution.CBOR.CodecOptions = Evolution.CBOR.CML_DEFAULT_OPTIONS +): Evolution.CBOR.CodecOptions => { + switch (preset) { + case "canonical": + return Evolution.CBOR.CANONICAL_OPTIONS + case "cml": + return Evolution.CBOR.CML_DEFAULT_OPTIONS + case "cml-data": + return Evolution.CBOR.CML_DATA_DEFAULT_OPTIONS + case "aiken": + return Evolution.CBOR.AIKEN_DEFAULT_OPTIONS + case "struct-friendly": + return Evolution.CBOR.STRUCT_FRIENDLY_OPTIONS + case "cardano-node-data": + return Evolution.CBOR.CARDANO_NODE_DATA_OPTIONS + default: + return fallback + } +} + +const serializeCborValue = (value: Evolution.CBOR.CBOR): StructuredCborValue => { + if (typeof value === "bigint") { + return { type: "integer", value: value.toString() } + } + + if (value instanceof Uint8Array) { + return { type: "bytes", hex: bytesToHex(value) } + } + + if (typeof value === "string") { + return { type: "text", value } + } + + if (Array.isArray(value)) { + return { type: "array", items: value.map(serializeCborValue) } + } + + if (value instanceof Map) { + return { + type: "map", + entries: Array.from(value.entries()).map(([key, entryValue]) => ({ + key: serializeCborValue(key), + value: serializeCborValue(entryValue) + })) + } + } + + if (Evolution.CBOR.BoundedBytes.is(value)) { + return { type: "boundedBytes", hex: bytesToHex(value.bytes) } + } + + if (Evolution.CBOR.isTag(value)) { + return { + type: "tag", + tag: value.tag, + value: serializeCborValue(value.value) + } + } + + if (typeof value === "boolean") { + return { type: "boolean", value } + } + + if (value === null) { + return { type: "null" } + } + + if (value === undefined) { + return { type: "undefined" } + } + + if (typeof value === "number") { + return { type: "float", value } + } + + return { + type: "record", + entries: Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, serializeCborValue(entryValue)])) + } +} + +const serializeCborLengthEncoding = (value: Evolution.CBOR.LengthEncoding | undefined) => + value + ? value.tag === "indefinite" + ? { tag: "indefinite" as const } + : { tag: "definite" as const, byteSize: value.byteSize } + : undefined + +const serializeCborStringEncoding = (value: Evolution.CBOR.StringEncoding | undefined) => + value + ? value.tag === "definite" + ? { tag: "definite" as const, byteSize: value.byteSize } + : { + tag: "indefinite" as const, + chunks: value.chunks.map((chunk) => ({ length: chunk.length, byteSize: chunk.byteSize })) + } + : undefined + +const serializeCborFormat = (format: Evolution.CBOR.CBORFormat): unknown => { + switch (format._tag) { + case "uint": + case "nint": + return { + type: format._tag, + ...(format.byteSize !== undefined ? { byteSize: format.byteSize } : undefined) + } + case "bytes": + case "text": + return { + type: format._tag, + ...(format.encoding ? { encoding: serializeCborStringEncoding(format.encoding) } : undefined) + } + case "array": + return { + type: "array", + ...(format.length ? { length: serializeCborLengthEncoding(format.length) } : undefined), + children: format.children.map(serializeCborFormat) + } + case "map": + return { + type: "map", + ...(format.length ? { length: serializeCborLengthEncoding(format.length) } : undefined), + ...(format.keyOrder ? { keyOrderHex: format.keyOrder.map(bytesToHex) } : undefined), + entries: format.entries.map(([key, value]) => [serializeCborFormat(key), serializeCborFormat(value)]) + } + case "tag": + return { + type: "tag", + ...(format.width !== undefined ? { width: format.width } : undefined), + child: serializeCborFormat(format.child) + } + case "simple": + return { type: "simple" } + } +} + +const parseStructuredCbor = (value: unknown): Evolution.CBOR.CBOR => { + if (!isObject(value) || typeof value.type !== "string") { + throw new Error("CBOR value must be a tagged object with a string type field") + } + + switch (value.type) { + case "integer": + if (typeof value.value !== "string") { + throw new Error("CBOR integer value must be a string") + } + return BigInt(value.value) + case "bytes": + if (typeof value.hex !== "string") { + throw new Error("CBOR bytes hex must be a string") + } + return hexToBytes(value.hex) + case "text": + if (typeof value.value !== "string") { + throw new Error("CBOR text value must be a string") + } + return value.value + case "array": + if (!Array.isArray(value.items)) { + throw new Error("CBOR array items must be an array") + } + return value.items.map(parseStructuredCbor) + case "map": + if (!Array.isArray(value.entries)) { + throw new Error("CBOR map entries must be an array") + } + return new Map( + value.entries.map((entry, index) => { + if (!isObject(entry) || !("key" in entry) || !("value" in entry)) { + throw new Error(`CBOR map entry ${index} must include key and value`) + } + + return [parseStructuredCbor(entry.key), parseStructuredCbor(entry.value)] as const + }) + ) + case "record": + if (!isObject(value.entries)) { + throw new Error("CBOR record entries must be an object") + } + return Object.fromEntries(Object.entries(value.entries).map(([key, entryValue]) => [key, parseStructuredCbor(entryValue)])) + case "tag": + if (typeof value.tag !== "number" || !Number.isInteger(value.tag)) { + throw new Error("CBOR tag value must be an integer number") + } + if (!("value" in value)) { + throw new Error("CBOR tag must include a nested value") + } + return Evolution.CBOR.Tag.make({ tag: value.tag, value: parseStructuredCbor(value.value) }) + case "boolean": + if (typeof value.value !== "boolean") { + throw new Error("CBOR boolean value must be a boolean") + } + return value.value + case "null": + return null + case "undefined": + return undefined + case "float": + if (typeof value.value !== "number") { + throw new Error("CBOR float value must be a number") + } + return value.value + case "boundedBytes": + if (typeof value.hex !== "string") { + throw new Error("CBOR boundedBytes hex must be a string") + } + return Evolution.CBOR.BoundedBytes.make(hexToBytes(value.hex)) + default: + throw new Error(`Unsupported CBOR type: ${value.type}`) + } +} + +const serializeDataValue = (value: Evolution.Data.Data): StructuredDataValue => { + if (value instanceof Evolution.Data.Constr) { + return { + type: "constr", + index: value.index.toString(), + fields: value.fields.map((field) => serializeDataValue(field as Evolution.Data.Data)) + } + } + + if (value instanceof Map) { + return { + type: "map", + entries: Array.from(value.entries()).map(([key, entryValue]) => ({ + key: serializeDataValue(key as Evolution.Data.Data), + value: serializeDataValue(entryValue as Evolution.Data.Data) + })) + } + } + + if (Array.isArray(value)) { + return { + type: "list", + items: value.map((item) => serializeDataValue(item as Evolution.Data.Data)) + } + } + + if (typeof value === "bigint") { + return { type: "int", value: value.toString() } + } + + if (value instanceof Uint8Array) { + return { type: "bytes", hex: bytesToHex(value) } + } + + throw new Error("Unsupported Data value") +} + +const parseStructuredData = (value: unknown): Evolution.Data.Data => { + if (!isObject(value) || typeof value.type !== "string") { + throw new Error("Data value must be a tagged object with a string type field") + } + + switch (value.type) { + case "constr": + if (typeof value.index !== "string") { + throw new Error("Data constr index must be a string") + } + if (!Array.isArray(value.fields)) { + throw new Error("Data constr fields must be an array") + } + return Evolution.Data.constr( + BigInt(value.index), + value.fields.map((field) => parseStructuredData(field)) + ) + case "map": + if (!Array.isArray(value.entries)) { + throw new Error("Data map entries must be an array") + } + return Evolution.Data.map( + value.entries.map((entry, index) => { + if (!isObject(entry) || !("key" in entry) || !("value" in entry)) { + throw new Error(`Data map entry ${index} must include key and value`) + } + + return [parseStructuredData(entry.key), parseStructuredData(entry.value)] + }) + ) + case "list": + if (!Array.isArray(value.items)) { + throw new Error("Data list items must be an array") + } + return Evolution.Data.list(value.items.map((item) => parseStructuredData(item))) + case "int": + if (typeof value.value !== "string") { + throw new Error("Data int value must be a string") + } + return Evolution.Data.int(BigInt(value.value)) + case "bytes": + if (typeof value.hex !== "string") { + throw new Error("Data bytes hex must be a string") + } + return Evolution.Data.bytearray(value.hex) + default: + throw new Error(`Unsupported Data type: ${value.type}`) + } +} + +const parseAddressInput = (value: string): Evolution.Address.Address => { + try { + return Evolution.Address.fromBech32(value) + } catch { + return Evolution.Address.fromHex(value) + } +} + +const serializeCredential = (credential: Evolution.Credential.Credential): StructuredIdentifier => + credential._tag === "KeyHash" + ? { + type: "credential", + credentialType: "keyHash", + hex: Evolution.KeyHash.toHex(credential), + cborHex: Evolution.Credential.toCBORHex(credential) + } + : { + type: "credential", + credentialType: "scriptHash", + hex: Evolution.ScriptHash.toHex(credential), + cborHex: Evolution.Credential.toCBORHex(credential) + } + +const parseStructuredCredential = (value: unknown): Evolution.Credential.Credential => { + if (!isObject(value) || value.type !== "credential") { + throw new Error("Credential value must be a tagged credential object") + } + + if (value.credentialType === "keyHash") { + if (typeof value.hex !== "string") { + throw new Error("Credential keyHash hex must be a string") + } + return Evolution.KeyHash.fromHex(value.hex) + } + + if (value.credentialType === "scriptHash") { + if (typeof value.hex !== "string") { + throw new Error("Credential scriptHash hex must be a string") + } + return Evolution.ScriptHash.fromHex(value.hex) + } + + throw new Error("Credential credentialType must be keyHash or scriptHash") +} + +const serializeDRep = (drep: Evolution.DRep.DRep): StructuredIdentifier => { + switch (drep._tag) { + case "KeyHashDRep": + return { + type: "drep", + drepType: "keyHash", + hex: Evolution.DRep.toHex(drep), + bech32: Evolution.DRep.toBech32(drep), + cborHex: Evolution.DRep.toCBORHex()(drep) + } + case "ScriptHashDRep": + return { + type: "drep", + drepType: "scriptHash", + hex: Evolution.DRep.toHex(drep), + bech32: Evolution.DRep.toBech32(drep), + cborHex: Evolution.DRep.toCBORHex()(drep) + } + case "AlwaysAbstainDRep": + return { + type: "drep", + drepType: "alwaysAbstain", + cborHex: Evolution.DRep.toCBORHex()(drep) + } + case "AlwaysNoConfidenceDRep": + return { + type: "drep", + drepType: "alwaysNoConfidence", + cborHex: Evolution.DRep.toCBORHex()(drep) + } + } +} + +const parseStructuredDRep = (value: unknown): Evolution.DRep.DRep => { + if (!isObject(value) || value.type !== "drep") { + throw new Error("DRep value must be a tagged drep object") + } + + switch (value.drepType) { + case "keyHash": + case "scriptHash": + if (typeof value.hex !== "string") { + throw new Error("DRep keyHash/scriptHash hex must be a string") + } + return Evolution.Schema.decodeSync(Evolution.DRep.FromHex)(value.hex) + case "alwaysAbstain": + return Evolution.DRep.alwaysAbstain() + case "alwaysNoConfidence": + return Evolution.DRep.alwaysNoConfidence() + default: + throw new Error("DRep drepType must be keyHash, scriptHash, alwaysAbstain, or alwaysNoConfidence") + } +} + +const serializeIdentifier = ( + kind: z.infer, + value: + | Evolution.KeyHash.KeyHash + | Evolution.ScriptHash.ScriptHash + | Evolution.PolicyId.PolicyId + | Evolution.PoolKeyHash.PoolKeyHash + | Evolution.DatumHash.DatumHash + | Evolution.TransactionHash.TransactionHash + | Evolution.Credential.Credential + | Evolution.DRep.DRep +): StructuredIdentifier => { + switch (kind) { + case "keyHash": + return { type: "keyHash", hex: Evolution.KeyHash.toHex(value as Evolution.KeyHash.KeyHash) } + case "scriptHash": + return { type: "scriptHash", hex: Evolution.ScriptHash.toHex(value as Evolution.ScriptHash.ScriptHash) } + case "policyId": + return { type: "policyId", hex: Evolution.PolicyId.toHex(value as Evolution.PolicyId.PolicyId) } + case "poolKeyHash": { + const poolKeyHash = value as Evolution.PoolKeyHash.PoolKeyHash + return { + type: "poolKeyHash", + hex: Evolution.PoolKeyHash.toHex(poolKeyHash), + bech32: Evolution.PoolKeyHash.toBech32(poolKeyHash) + } + } + case "datumHash": + return { type: "datumHash", hex: Evolution.DatumHash.toHex(value as Evolution.DatumHash.DatumHash) } + case "transactionHash": + return { type: "transactionHash", hex: Evolution.TransactionHash.toHex(value as Evolution.TransactionHash.TransactionHash) } + case "credential": + return serializeCredential(value as Evolution.Credential.Credential) + case "drep": + return serializeDRep(value as Evolution.DRep.DRep) + } +} + +const parseIdentifier = ( + kind: z.infer, + value: string, + format: "hex" | "bech32" | "cbor" +): + | Evolution.KeyHash.KeyHash + | Evolution.ScriptHash.ScriptHash + | Evolution.PolicyId.PolicyId + | Evolution.PoolKeyHash.PoolKeyHash + | Evolution.DatumHash.DatumHash + | Evolution.TransactionHash.TransactionHash + | Evolution.Credential.Credential + | Evolution.DRep.DRep => { + switch (kind) { + case "keyHash": + if (format !== "hex") throw new Error("keyHash only supports hex input") + return Evolution.KeyHash.fromHex(value) + case "scriptHash": + if (format !== "hex") throw new Error("scriptHash only supports hex input") + return Evolution.ScriptHash.fromHex(value) + case "policyId": + if (format !== "hex") throw new Error("policyId only supports hex input") + return Evolution.PolicyId.fromHex(value) + case "poolKeyHash": + if (format === "hex") return Evolution.PoolKeyHash.fromHex(value) + if (format === "bech32") return Evolution.PoolKeyHash.fromBech32(value) + throw new Error("poolKeyHash supports hex or bech32 input") + case "datumHash": + if (format !== "hex") throw new Error("datumHash only supports hex input") + return Evolution.DatumHash.fromHex(value) + case "transactionHash": + if (format !== "hex") throw new Error("transactionHash only supports hex input") + return Evolution.TransactionHash.fromHex(value) + case "credential": + if (format !== "cbor") throw new Error("credential only supports cbor input") + return Evolution.Credential.fromCBORHex(value) + case "drep": + if (format === "hex") return Evolution.Schema.decodeSync(Evolution.DRep.FromHex)(value) + if (format === "bech32") return Evolution.Schema.decodeSync(Evolution.DRep.FromBech32)(value) + if (format === "cbor") return Evolution.DRep.fromCBORHex(value) + throw new Error("drep supports hex, bech32, or cbor input") + } +} + +const serializeAssets = (assets: Evolution.Assets.Assets) => { + const record: Record = { + lovelace: assets.lovelace.toString() + } + + if (assets.multiAsset) { + for (const [policyId, assetMap] of assets.multiAsset.map.entries()) { + const policyIdHex = Evolution.PolicyId.toHex(policyId) + for (const [assetName, quantity] of assetMap.entries()) { + const assetNameHex = Evolution.AssetName.toHex(assetName) + record[`${policyIdHex}${assetNameHex}`] = quantity.toString() + } + } + } + + return { + record, + json: toStructured(assets.toJSON()), + isZero: Evolution.Assets.isZero(assets), + allPositive: Evolution.Assets.allPositive(assets) + } +} + +const listExportMembers = (exportName: string) => { + const exportValue = (Evolution as Record)[exportName] + + if (exportValue === undefined) { + throw new Error(`Unknown public export: ${exportName}`) + } + + if ((typeof exportValue !== "object" || exportValue === null) && typeof exportValue !== "function") { + return [{ name: exportName, type: typeof exportValue, callable: false }] + } + + return Object.entries(exportValue as Record) + .map(([name, member]) => ({ + name, + type: typeof member, + callable: typeof member === "function" + })) + .sort((left, right) => left.name.localeCompare(right.name)) +} + +const getClientCapabilities = (client: unknown) => ({ + canAttachProvider: hasMethod(client, "attachProvider"), + canAttachWallet: hasMethod(client, "attachWallet"), + canBuildTransactions: hasMethod(client, "newTx"), + hasProviderQueries: hasMethod(client, "getProtocolParameters"), + hasAddress: hasMethod(client, "address"), + hasRewardAddress: hasMethod(client, "rewardAddress"), + hasWalletUtxos: hasMethod(client, "getWalletUtxos"), + hasWalletDelegation: hasMethod(client, "getWalletDelegation"), + canSubmitTransaction: hasMethod(client, "submitTx") +}) + +const createServerResourceContents = () => ({ + uri: "evolution://catalog", + mimeType: "application/json", + text: JSON.stringify( + { + package: implementation, + toolGroups: [ + "sdk_info", + "sdk_exports", + "destroy_handle", + "address_codec", + "assets_codec", + "cbor_codec", + "data_codec", + "identifier_codec", + "typed_export_codec", + "evaluator_info", + "create_client", + "client_attach", + "client_invoke", + "tx_builder_create", + "tx_builder_apply", + "tx_builder_build", + "result_call", + "time_slot_convert", + "blueprint_parse", + "blueprint_codegen", + "message_sign", + "message_verify", + "fee_validate", + "encoding_codec", + "key_generate", + "native_script_tools", + "utxo_tools", + "address_build", + "metadata_tools", + "credential_tools", + "drep_tools", + "value_tools", + "assets_tools", + "unit_tools", + "coin_tools", + "network_tools", + "data_construct", + "hash_tools", + "mint_tools", + "withdrawals_tools", + "anchor_tools", + "certificate_tools", + "redeemer_tools", + "voting_tools", + "script_ref_tools", + "governance_action_tools", + "proposal_tools", + "tx_output_tools", + "plutus_data_codec_tools", + "pool_params_tools", + "drep_cert_tools", + "committee_cert_tools", + "constitution_tools", + "protocol_param_update_tools", + "transaction_input_tools", + "transaction_body_tools", + "pointer_address_tools", + "plutus_value_tools", + "script_tools", + "bip32_key_tools", + "byron_address_tools", + "uplc_tools", + "ed25519_signature_tools", + "redeemers_collection_tools", + "proposal_procedures_collection_tools", + "devnet" + ], + notes: [ + "Handles are opaque server-side session identifiers.", + "Client and builder APIs are intentionally grouped into workflow tools.", + "The current package covers stateless codecs, evaluators, time/slot conversion, CIP-57 blueprint parsing/codegen, CIP-8/CIP-30 message signing, fee validation, CIP-68 metadata codec, key generation/derivation, native script building, UTxO set operations, bech32/bytes codecs, address building from credentials, transaction metadata/AuxiliaryData construction, credential building, DRep governance tools, Value/Assets arithmetic and construction, CIP-67 unit/label tools, Coin arithmetic, network ID conversion, Plutus Data construction/matching, transaction hashing, Mint construction for minting/burning, Withdrawals for reward claiming, governance Anchors, certificate building (staking/delegation/governance), Redeemer/ExUnits for script validation, VotingProcedures for governance voting, ScriptRef for output references, client sessions, provider access, transaction building/signing flows, and local devnet management.", + "Use sdk_exports to inspect the current public @evolution-sdk/evolution export surface at runtime." + ], + publicExports: evolutionExports + }, + null, + 2 + ) +}) + +export const createEvolutionMcpServer = (): McpServer => { + const server = new McpServer(implementation, { + capabilities: { + tools: {}, + resources: {} + } + }) + + server.registerResource( + "evolution-catalog", + "evolution://catalog", + { + description: "Current Evolution MCP tool catalog", + mimeType: "application/json" + }, + async () => ({ + contents: [createServerResourceContents()] + }) + ) + + server.registerTool( + "sdk_info", + { + description: "Server metadata, session counts, and tool groups" + }, + async () => { + const result = { + implementation, + sessionStats: sessionStore.stats(), + exportCount: evolutionExports.length, + currentScope: [ + "public export introspection", + "stateless codecs", + "cbor and plutus data codecs", + "client sessions", + "provider queries", + "transaction builder sessions", + "sign/submit flows" + ] + } + + return toolTextResult(result) + } + ) + + server.registerTool( + "sdk_exports", + { + description: "List SDK root exports or inspect members of one export", + inputSchema: z.object({ + exportName: ExportNameSchema.optional() + }) + }, + async ({ exportName }) => { + const result = exportName + ? { + exportName, + members: listExportMembers(exportName) + } + : { + exports: evolutionExports, + total: evolutionExports.length + } + + return toolTextResult(result) + } + ) + + server.registerTool( + "destroy_handle", + { + description: "Delete a session handle", + inputSchema: z.object({ handle: z.string() }) + }, + async ({ handle }) => { + const deleted = sessionStore.delete(handle) + const result = { handle, deleted, sessionStats: sessionStore.stats() } + return toolTextResult(result) + } + ) + + server.registerTool( + "address_codec", + { + description: "Inspect or convert Cardano addresses (bech32/hex)", + inputSchema: z.object({ + action: z.enum(["inspect", "toBech32", "toHex"]), + value: z.string() + }) + }, + async ({ action, value }) => { + const parsed = parseAddressInput(value) + const result = + action === "inspect" + ? { + details: toStructured( + Evolution.Address.getAddressDetails(value) ?? { + address: { + bech32: Evolution.Address.toBech32(parsed), + hex: Evolution.Address.toHex(parsed) + }, + networkId: Evolution.Address.getNetworkId(parsed), + type: Evolution.Address.isEnterprise(parsed) ? "Enterprise" : "Base", + paymentCredential: parsed.paymentCredential.toJSON(), + stakingCredential: parsed.stakingCredential?.toJSON() + } + ) + } + : action === "toBech32" + ? { bech32: Evolution.Address.toBech32(parsed) } + : { hex: Evolution.Address.toHex(parsed) } + + return toolTextResult(result) + } + ) + + server.registerTool( + "assets_codec", + { + description: "Inspect and combine Assets values", + inputSchema: z.object({ + action: z.enum(["inspect", "merge", "subtract", "negate", "addByHex"]), + record: AssetRecordSchema.optional(), + left: AssetRecordSchema.optional(), + right: AssetRecordSchema.optional(), + delta: AssetDeltaSchema.optional() + }) + }, + async ({ action, record, left, right, delta }) => { + let result: ToolResultObject + + switch (action) { + case "inspect": { + if (!record) { + throw new Error("record is required for inspect") + } + result = { assets: serializeAssets(parseAssets(record as AssetRecordInput)) } + break + } + case "merge": { + if (!left || !right) { + throw new Error("left and right are required for merge") + } + result = { + assets: serializeAssets( + Evolution.Assets.merge(parseAssets(left as AssetRecordInput), parseAssets(right as AssetRecordInput)) + ) + } + break + } + case "subtract": { + if (!left || !right) { + throw new Error("left and right are required for subtract") + } + result = { + assets: serializeAssets( + Evolution.Assets.subtract(parseAssets(left as AssetRecordInput), parseAssets(right as AssetRecordInput)) + ) + } + break + } + case "negate": { + if (!record) { + throw new Error("record is required for negate") + } + result = { assets: serializeAssets(Evolution.Assets.negate(parseAssets(record as AssetRecordInput))) } + break + } + case "addByHex": { + if (!record || !delta) { + throw new Error("record and delta are required for addByHex") + } + result = { + assets: serializeAssets( + Evolution.Assets.addByHex( + parseAssets(record as AssetRecordInput), + delta.policyIdHex, + delta.assetNameHex, + parseBigInt(delta.quantity) + ) + ) + } + break + } + } + + return toolTextResult(result) + } + ) + + server.registerTool( + "cbor_codec", + { + description: "Decode, encode, compare CBOR values", + inputSchema: z.object({ + action: z.enum(["decode", "decodeWithFormat", "encode", "reencode", "equals"]), + cborHex: z.string().optional(), + leftCborHex: z.string().optional(), + rightCborHex: z.string().optional(), + value: z.unknown().optional(), + optionsPreset: CborOptionsPresetSchema + }) + }, + async ({ action, cborHex, leftCborHex, rightCborHex, value, optionsPreset }) => { + const options = resolveCborOptions(optionsPreset) + + const result = + action === "decode" + ? (() => { + if (!cborHex) { + throw new Error("cborHex is required for decode") + } + return { value: serializeCborValue(Evolution.CBOR.fromCBORHex(cborHex, options)) } + })() + : action === "decodeWithFormat" + ? (() => { + if (!cborHex) { + throw new Error("cborHex is required for decodeWithFormat") + } + const decoded = Evolution.CBOR.fromCBORHexWithFormat(cborHex) + return { + value: serializeCborValue(decoded.value), + format: serializeCborFormat(decoded.format) + } + })() + : action === "encode" + ? (() => { + if (value === undefined) { + throw new Error("value is required for encode") + } + const parsed = parseStructuredCbor(value) + return { + cborHex: Evolution.CBOR.toCBORHex(parsed, options), + value: serializeCborValue(parsed) + } + })() + : action === "reencode" + ? (() => { + if (!cborHex) { + throw new Error("cborHex is required for reencode") + } + const parsed = Evolution.CBOR.fromCBORHex(cborHex, options) + return { + cborHex: Evolution.CBOR.toCBORHex(parsed, options), + value: serializeCborValue(parsed) + } + })() + : (() => { + if (!leftCborHex || !rightCborHex) { + throw new Error("leftCborHex and rightCborHex are required for equals") + } + return { + equal: Evolution.CBOR.equals( + Evolution.CBOR.fromCBORHex(leftCborHex, options), + Evolution.CBOR.fromCBORHex(rightCborHex, options) + ) + } + })() + + return toolTextResult(result) + } + ) + + server.registerTool( + "data_codec", + { + description: "Decode, encode, hash Plutus Data values", + inputSchema: z.object({ + action: z.enum(["decode", "encode", "reencode", "hashData", "equals"]), + dataCborHex: z.string().optional(), + leftDataCborHex: z.string().optional(), + rightDataCborHex: z.string().optional(), + value: z.unknown().optional(), + optionsPreset: CborOptionsPresetSchema + }) + }, + async ({ action, dataCborHex, leftDataCborHex, rightDataCborHex, value, optionsPreset }) => { + const options = resolveCborOptions(optionsPreset, Evolution.CBOR.CML_DATA_DEFAULT_OPTIONS) + + const result = + action === "decode" + ? (() => { + if (!dataCborHex) { + throw new Error("dataCborHex is required for decode") + } + return { data: serializeDataValue(Evolution.Data.fromCBORHex(dataCborHex, options)) } + })() + : action === "encode" + ? (() => { + if (value === undefined) { + throw new Error("value is required for encode") + } + const parsed = parseStructuredData(value) + return { + cborHex: Evolution.Data.toCBORHex(parsed, options), + data: serializeDataValue(parsed) + } + })() + : action === "reencode" + ? (() => { + if (!dataCborHex) { + throw new Error("dataCborHex is required for reencode") + } + const parsed = Evolution.Data.fromCBORHex(dataCborHex, options) + return { + cborHex: Evolution.Data.toCBORHex(parsed, options), + data: serializeDataValue(parsed) + } + })() + : action === "hashData" + ? (() => { + const parsed = dataCborHex + ? Evolution.Data.fromCBORHex(dataCborHex, options) + : value === undefined + ? (() => { + throw new Error("dataCborHex or value is required for hashData") + })() + : parseStructuredData(value) + + const datumHash = Evolution.Data.hashData(parsed, options) + return { + data: serializeDataValue(parsed), + datumHash: datumHash.toJSON().hash, + structuralHash: Evolution.Data.hash(parsed) + } + })() + : (() => { + if (!leftDataCborHex || !rightDataCborHex) { + throw new Error("leftDataCborHex and rightDataCborHex are required for equals") + } + return { + equal: Evolution.Data.equals( + Evolution.Data.fromCBORHex(leftDataCborHex, options), + Evolution.Data.fromCBORHex(rightDataCborHex, options) + ) + } + })() + + return toolTextResult(result) + } + ) + + server.registerTool( + "identifier_codec", + { + description: "Inspect and convert hashes, credentials, keys", + inputSchema: z.object({ + kind: IdentifierKindSchema, + action: z.enum(["decode", "encode", "equals"]), + input: z.string().optional(), + inputFormat: z.enum(["hex", "bech32", "cbor"]).optional(), + left: z.string().optional(), + leftFormat: z.enum(["hex", "bech32", "cbor"]).optional(), + right: z.string().optional(), + rightFormat: z.enum(["hex", "bech32", "cbor"]).optional(), + value: z.unknown().optional() + }) + }, + async ({ kind, action, input, inputFormat, left, leftFormat, right, rightFormat, value }) => { + const result = + action === "decode" + ? (() => { + if (!input || !inputFormat) { + throw new Error("input and inputFormat are required for decode") + } + return { + identifier: serializeIdentifier(kind, parseIdentifier(kind, input, inputFormat)) + } + })() + : action === "encode" + ? (() => { + if (value === undefined) { + throw new Error("value is required for encode") + } + + const encoded = + kind === "credential" + ? serializeCredential(parseStructuredCredential(value)) + : kind === "drep" + ? serializeDRep(parseStructuredDRep(value)) + : (() => { + throw new Error("encode is only supported for credential and drep structured values") + })() + + return { identifier: encoded } + })() + : (() => { + if (!left || !leftFormat || !right || !rightFormat) { + throw new Error("left, leftFormat, right, and rightFormat are required for equals") + } + + const leftIdentifier = serializeIdentifier(kind, parseIdentifier(kind, left, leftFormat)) + const rightIdentifier = serializeIdentifier(kind, parseIdentifier(kind, right, rightFormat)) + + return { + equal: JSON.stringify(leftIdentifier) === JSON.stringify(rightIdentifier), + left: leftIdentifier, + right: rightIdentifier + } + })() + + return toolTextResult(result) + } + ) + + server.registerTool( + "typed_export_codec", + { + description: + "Decode, re-encode, or manipulate any Evolution SDK typed export via CBOR. " + + "Covers Transaction, TransactionWitnessSet, Script, Certificate, Redeemer, " + + "TransactionBody, TransactionOutput, Value, Mint, and many more.", + inputSchema: z.object({ + moduleName: z.string().refine((value) => typedExportModules.has(value), { + message: `Module must be one of: ${typedExportModuleNames.join(", ")}` + }), + action: z.enum(["decode", "reencode", "listModules", "addVKeyWitnessesHex"]), + cborHex: z.string().optional(), + witnessSetCborHex: z.string().optional(), + cborOptionsPreset: CborOptionsPresetSchema + }) + }, + async ({ moduleName, action, cborHex, witnessSetCborHex, cborOptionsPreset }) => { + if (action === "listModules") { + return toolTextResult({ modules: typedExportModuleNames }) + } + + if (!cborHex) { + throw new Error("cborHex is required for decode, reencode, and addVKeyWitnessesHex actions") + } + + if (action === "addVKeyWitnessesHex") { + if (moduleName !== "Transaction") { + throw new Error("addVKeyWitnessesHex is only supported for moduleName 'Transaction'") + } + if (!witnessSetCborHex) { + throw new Error("witnessSetCborHex is required for addVKeyWitnessesHex") + } + const merged = Evolution.Transaction.addVKeyWitnessesHex(cborHex, witnessSetCborHex) + return toolTextResult({ + moduleName, + cborHex: merged, + transaction: serializeTransaction(Evolution.Transaction.fromCBORHex(merged)) + }) + } + + const mod = typedExportModules.get(moduleName) + if (!mod) { + throw new Error(`Module ${moduleName} does not support typed CBOR codec`) + } + + const options = resolveCborOptions(cborOptionsPreset) + const decoded = mod.fromCBORHex(cborHex, options) + + if (action === "reencode") { + const reencoded = mod.toCBORHex(decoded, options) + return toolTextResult({ moduleName, cborHex: reencoded }) + } + + const json = hasMethod(decoded, "toJSON") ? toStructured(decoded.toJSON()) : toStructured(decoded) + + return toolTextResult({ moduleName, json, cborHex: mod.toCBORHex(decoded, options) }) + } + ) + + server.registerTool( + "create_client", + { + description: "Create an Evolution client session", + inputSchema: z.object({ + network: NetworkSchema.optional(), + provider: ProviderConfigSchema.optional(), + wallet: WalletConfigSchema.optional(), + slotConfig: SlotConfigSchema + }) + }, + async ({ network, provider, wallet, slotConfig }) => { + const config = { + ...(network !== undefined ? { network } : undefined), + ...(provider ? { provider } : undefined), + ...(wallet ? { wallet } : undefined), + ...(slotConfig + ? { + slotConfig: { + zeroTime: parseBigInt(slotConfig.zeroTime), + zeroSlot: parseBigInt(slotConfig.zeroSlot), + slotLength: slotConfig.slotLength + } + } + : undefined) + } + + const client = Evolution.createClient(config) + const capabilities = getClientCapabilities(client) + const clientHandle = sessionStore.createClient(client, capabilities) + const result = { clientHandle, capabilities } + + return toolTextResult(result) + } + ) + + server.registerTool( + "client_attach", + { + description: "Attach a provider or wallet to a client session", + inputSchema: z.object({ + clientHandle: z.string(), + type: z.enum(["provider", "wallet"]), + provider: ProviderConfigSchema.optional(), + wallet: WalletConfigSchema.optional() + }) + }, + async ({ clientHandle, type, provider, wallet }) => { + const session = sessionStore.getClient(clientHandle) + + if (type === "provider") { + if (!provider) throw new Error("provider config is required when type is 'provider'") + if (!hasMethod(session.client, "attachProvider")) { + throw new Error(`Client handle ${clientHandle} does not support attachProvider()`) + } + const attached = session.client.attachProvider(provider) + const capabilities = getClientCapabilities(attached) + const attachedClientHandle = sessionStore.createClient(attached, capabilities) + return toolTextResult({ attachedClientHandle, capabilities }) + } + + if (!wallet) throw new Error("wallet config is required when type is 'wallet'") + if (!hasMethod(session.client, "attachWallet")) { + throw new Error(`Client handle ${clientHandle} does not support attachWallet()`) + } + const attached = session.client.attachWallet(wallet) + const capabilities = getClientCapabilities(attached) + const attachedClientHandle = sessionStore.createClient(attached, capabilities) + return toolTextResult({ attachedClientHandle, capabilities }) + } + ) + + server.registerTool( + "client_invoke", + { + description: "Invoke a wallet or provider method on a client", + inputSchema: z.object({ + clientHandle: z.string(), + method: z.enum([ + "address", + "rewardAddress", + "getProtocolParameters", + "getWalletUtxos", + "getWalletDelegation", + "getUtxos", + "getUtxosWithUnit", + "getUtxoByUnit", + "submitTx", + "awaitTx", + "evaluateTx" + ]), + address: z.string().optional(), + unit: z.string().optional(), + transactionCborHex: z.string().optional(), + txHash: z.string().optional(), + checkInterval: z.number().int().positive().optional(), + timeout: z.number().int().positive().optional() + }) + }, + async ({ clientHandle, method, address, unit, transactionCborHex, txHash, checkInterval, timeout }) => { + const { client } = sessionStore.getClient(clientHandle) + + let result: ToolResultObject + switch (method) { + case "address": + if (!hasMethod(client, "address")) { + throw new Error(`Client handle ${clientHandle} does not expose address()`) + } + result = { address: serializeAddress(await client.address()) } + break + case "rewardAddress": + if (!hasMethod(client, "rewardAddress")) { + throw new Error(`Client handle ${clientHandle} does not expose rewardAddress()`) + } + result = { rewardAddress: (await client.rewardAddress()) ?? null } + break + case "getProtocolParameters": + if (!hasMethod(client, "getProtocolParameters")) { + throw new Error(`Client handle ${clientHandle} does not expose getProtocolParameters()`) + } + result = serializeProtocolParameters(await client.getProtocolParameters()) as ToolResultObject + break + case "getWalletUtxos": + if (!hasMethod(client, "getWalletUtxos")) { + throw new Error(`Client handle ${clientHandle} does not expose getWalletUtxos()`) + } + result = { utxos: serializeUtxos(await client.getWalletUtxos()) } + break + case "getWalletDelegation": + if (!hasMethod(client, "getWalletDelegation")) { + throw new Error(`Client handle ${clientHandle} does not expose getWalletDelegation()`) + } + result = serializeDelegation(await client.getWalletDelegation()) + break + case "getUtxos": + if (!hasMethod(client, "getUtxos")) { + throw new Error(`Client handle ${clientHandle} does not expose getUtxos()`) + } + if (!address) { + throw new Error("address is required for getUtxos") + } + result = { utxos: serializeUtxos(await client.getUtxos(parseAddress(address))) } + break + case "getUtxosWithUnit": + if (!hasMethod(client, "getUtxosWithUnit")) { + throw new Error(`Client handle ${clientHandle} does not expose getUtxosWithUnit()`) + } + if (!address || !unit) { + throw new Error("address and unit are required for getUtxosWithUnit") + } + result = { utxos: serializeUtxos(await client.getUtxosWithUnit(parseAddress(address), unit)) } + break + case "getUtxoByUnit": + if (!hasMethod(client, "getUtxoByUnit")) { + throw new Error(`Client handle ${clientHandle} does not expose getUtxoByUnit()`) + } + if (!unit) { + throw new Error("unit is required for getUtxoByUnit") + } + result = { utxo: serializeUtxos([await client.getUtxoByUnit(unit)])[0] } + break + case "submitTx": + if (!hasMethod(client, "submitTx")) { + throw new Error(`Client handle ${clientHandle} does not expose submitTx()`) + } + if (!transactionCborHex) { + throw new Error("transactionCborHex is required for submitTx") + } + result = { txHash: serializeTransactionHash(await client.submitTx(parseTransaction(transactionCborHex))) } + break + case "awaitTx": + if (!hasMethod(client, "awaitTx")) { + throw new Error(`Client handle ${clientHandle} does not expose awaitTx()`) + } + if (!txHash) { + throw new Error("txHash is required for awaitTx") + } + result = { + confirmed: await client.awaitTx(Evolution.TransactionHash.fromHex(txHash), checkInterval, timeout) + } + break + case "evaluateTx": + if (!hasMethod(client, "evaluateTx")) { + throw new Error(`Client handle ${clientHandle} does not expose evaluateTx()`) + } + if (!transactionCborHex) { + throw new Error("transactionCborHex is required for evaluateTx") + } + result = { evaluation: toStructured(await client.evaluateTx(parseTransaction(transactionCborHex))) } + break + } + + return toolTextResult(result) + } + ) + + server.registerTool( + "tx_builder_create", + { + description: "Create a transaction builder from a client handle", + inputSchema: z.object({ + clientHandle: z.string(), + availableUtxos: z.array(UtxoInputSchema).optional() + }) + }, + async ({ clientHandle, availableUtxos }) => { + const { client } = sessionStore.getClient(clientHandle) + if (!hasMethod(client, "newTx")) { + throw new Error(`Client handle ${clientHandle} does not expose newTx()`) + } + + const builder = availableUtxos ? client.newTx(parseUtxos(availableUtxos)) : client.newTx() + const builderHandle = sessionStore.createBuilder(builder, clientHandle) + const result = { builderHandle, operations: [] } + + return toolTextResult(result) + } + ) + + server.registerTool( + "tx_builder_apply", + { + description: "Apply a builder operation to a tx builder handle", + inputSchema: z.object({ + builderHandle: z.string(), + operation: z.enum(["payToAddress", "collectFrom", "readFrom", "mintAssets", "setValidity", "sendAll"]), + address: z.string().optional(), + assets: AssetRecordSchema.optional(), + utxos: z.array(UtxoInputSchema).optional(), + fromUnixMs: z.union([z.string(), z.number().int()]).optional(), + toUnixMs: z.union([z.string(), z.number().int()]).optional(), + datumOptionCborHex: z.string().optional(), + scriptCborHex: z.string().optional() + }) + }, + async ({ builderHandle, operation, address, assets, utxos, fromUnixMs, toUnixMs, datumOptionCborHex, scriptCborHex }) => { + const session = sessionStore.getBuilder(builderHandle) + const builder = session.builder as Record) => unknown> + + switch (operation) { + case "payToAddress": + if (!address || !assets) { + throw new Error("address and assets are required for payToAddress") + } + builder.payToAddress({ + address: parseAddress(address), + assets: parseAssets(assets as AssetRecordInput), + datum: datumOptionCborHex ? Evolution.DatumOption.fromCBORHex(datumOptionCborHex) : undefined, + script: scriptCborHex ? Evolution.Script.fromCBORHex(scriptCborHex) : undefined + }) + break + case "collectFrom": + if (!utxos) { + throw new Error("utxos are required for collectFrom") + } + builder.collectFrom({ inputs: parseUtxos(utxos as Array) }) + break + case "readFrom": + if (!utxos) { + throw new Error("utxos are required for readFrom") + } + builder.readFrom({ referenceInputs: parseUtxos(utxos as Array) }) + break + case "mintAssets": + if (!assets) { + throw new Error("assets are required for mintAssets") + } + builder.mintAssets({ assets: parseAssets(assets as AssetRecordInput) }) + break + case "setValidity": + builder.setValidity({ + from: fromUnixMs === undefined ? undefined : parseBigInt(fromUnixMs), + to: toUnixMs === undefined ? undefined : parseBigInt(toUnixMs) + }) + break + case "sendAll": + if (!address) { + throw new Error("address is required for sendAll") + } + builder.sendAll({ to: parseAddress(address) }) + break + } + + sessionStore.updateBuilderOperations(builderHandle, operation) + const result = { builderHandle, operations: [...session.operations, operation] } + return toolTextResult(result) + } + ) + + server.registerTool( + "tx_builder_build", + { + description: "Build a transaction from a builder handle", + inputSchema: z.object({ + builderHandle: z.string(), + evaluator: EvaluatorSchema, + buildOptions: z + .object({ + changeAddress: z.string().optional(), + availableUtxos: z.array(UtxoInputSchema).optional(), + protocolParameters: ProtocolParametersSchema.optional(), + coinSelection: z.enum(["largest-first", "random-improve", "optimal"]).optional() + }) + .optional() + }) + }, + async ({ builderHandle, evaluator, buildOptions }) => { + const session = sessionStore.getBuilder(builderHandle) + const builder = session.builder as { build: (options?: Record) => Promise } + + const resolvedEvaluator = resolveEvaluator(evaluator) + + const parsedOptions = buildOptions + ? { + ...(buildOptions.changeAddress ? { changeAddress: parseAddress(buildOptions.changeAddress) } : undefined), + ...(buildOptions.availableUtxos ? { availableUtxos: parseUtxos(buildOptions.availableUtxos) } : undefined), + ...(buildOptions.protocolParameters + ? { protocolParameters: parseProtocolParameters(buildOptions.protocolParameters as ProtocolParametersInput) } + : undefined), + ...(buildOptions.coinSelection ? { coinSelection: buildOptions.coinSelection } : undefined), + ...(resolvedEvaluator ? { evaluator: resolvedEvaluator } : undefined) + } + : resolvedEvaluator + ? { evaluator: resolvedEvaluator } + : undefined + + const built = await builder.build(parsedOptions) + const transaction = await (built as { toTransaction: () => Promise }).toTransaction() + const estimatedFee = await (built as { estimateFee: () => Promise }).estimateFee() + + const isSignBuilder = hasMethod(built, "sign") + const resultHandle = sessionStore.createResult( + built, + isSignBuilder ? "sign-builder" : "transaction-result", + builderHandle + ) + + const result = { + resultHandle, + resultType: isSignBuilder ? "sign-builder" : "transaction-result", + estimatedFee: estimatedFee.toString(), + transaction: serializeTransaction(transaction), + chainResult: + isSignBuilder && hasMethod(built, "chainResult") + ? (() => { + const chainResult = built.chainResult() + return { + consumed: serializeUtxos(chainResult.consumed), + available: serializeUtxos(chainResult.available), + txHash: chainResult.txHash + } + })() + : null + } + + return toolTextResult(result) + } + ) + + server.registerTool( + "result_call", + { + description: "Sign, inspect, or submit a transaction result/submit handle", + inputSchema: z.object({ + handle: z.string(), + action: z.enum([ + "toTransaction", + "toTransactionWithFakeWitnesses", + "estimateFee", + "chainResult", + "sign", + "signAndSubmit", + "partialSign", + "getWitnessSet", + "signWithWitness", + "assemble", + "submit" + ]), + witnessSetCborHex: z.string().optional(), + witnessSetsCborHex: z.array(z.string()).optional() + }) + }, + async ({ handle, action, witnessSetCborHex, witnessSetsCborHex }) => { + // Submit actions operate on a submit handle + if (action === "submit" || (action === "getWitnessSet" && sessionStore.hasSubmit(handle))) { + const session = sessionStore.getSubmit(handle) + const submitBuilder = session.submitBuilder as SubmitBuilderLike + const result = + action === "getWitnessSet" + ? { witnessSet: serializeWitnessSet(submitBuilder.witnessSet) } + : { txHash: serializeTransactionHash(await submitBuilder.submit()) } + return toolTextResult(result) + } + + // All other actions operate on a result handle + const session = sessionStore.getResult(handle) + const resultBuilder = session.result as Record) => Promise> + + if (session.resultType !== "sign-builder" && !["toTransaction", "toTransactionWithFakeWitnesses", "estimateFee"].includes(action)) { + throw new Error(`Result handle ${handle} is not a SignBuilder`) + } + + let result: ToolResultObject + switch (action) { + case "toTransaction": + result = { transaction: serializeTransaction(await resultBuilder.toTransaction()) } + break + case "toTransactionWithFakeWitnesses": + result = { transaction: serializeTransaction(await resultBuilder.toTransactionWithFakeWitnesses()) } + break + case "estimateFee": + result = { estimatedFee: (await resultBuilder.estimateFee()).toString() } + break + case "chainResult": { + const chainResult = (session.result as { + chainResult: () => { + readonly consumed: ReadonlyArray + readonly available: ReadonlyArray + readonly txHash: string + } + }).chainResult() + result = { + consumed: serializeUtxos(chainResult.consumed), + available: serializeUtxos(chainResult.available), + txHash: chainResult.txHash + } + break + } + case "sign": { + const submitBuilder = await resultBuilder.sign() + const submitHandle = sessionStore.createSubmit(submitBuilder, handle) + result = { + submitHandle, + witnessSet: serializeWitnessSet((submitBuilder as SubmitBuilderLike).witnessSet) + } + break + } + case "signAndSubmit": + result = { txHash: serializeTransactionHash(await resultBuilder.signAndSubmit()) } + break + case "partialSign": + result = { witnessSet: serializeWitnessSet(await resultBuilder.partialSign()) } + break + case "getWitnessSet": + result = { witnessSet: serializeWitnessSet(await resultBuilder.getWitnessSet()) } + break + case "signWithWitness": { + if (!witnessSetCborHex) { + throw new Error("witnessSetCborHex is required for signWithWitness") + } + const submitBuilder = await resultBuilder.signWithWitness(parseWitnessSet(witnessSetCborHex)) + const submitHandle = sessionStore.createSubmit(submitBuilder, handle) + result = { + submitHandle, + witnessSet: serializeWitnessSet((submitBuilder as SubmitBuilderLike).witnessSet) + } + break + } + case "assemble": { + if (!witnessSetsCborHex || witnessSetsCborHex.length === 0) { + throw new Error("witnessSetsCborHex is required for assemble") + } + const submitBuilder = await resultBuilder.assemble(witnessSetsCborHex.map(parseWitnessSet)) + const submitHandle = sessionStore.createSubmit(submitBuilder, handle) + result = { + submitHandle, + witnessSet: serializeWitnessSet((submitBuilder as SubmitBuilderLike).witnessSet) + } + break + } + } + + return toolTextResult(result) + } + ) + + // ── Evaluator info ────────────────────────────────────────────────────── + + server.registerTool( + "evaluator_info", + { + description: + "List available UPLC evaluators from @evolution-sdk/aiken-uplc and @evolution-sdk/scalus-uplc. " + + "These can be passed to tx_builder_build via the 'evaluator' parameter.", + inputSchema: z.object({}) + }, + async () => { + const evaluators = [ + { + name: "aiken", + package: "@evolution-sdk/aiken-uplc", + description: "Aiken UPLC evaluator (Rust/WASM)", + available: typeof createAikenEvaluator?.evaluate === "function" + }, + { + name: "scalus", + package: "@evolution-sdk/scalus-uplc", + description: "Scalus UPLC evaluator (Scala/WASM)", + available: typeof createScalusEvaluator?.evaluate === "function" + } + ] + + return toolTextResult({ + evaluators, + usage: "Pass evaluator name ('aiken' or 'scalus') to tx_builder_build to enable Plutus script evaluation" + }) + } + ) + + // ── Time / slot conversion ────────────────────────────────────────────── + + const SlotConfigNetworkSchema = z.enum(["Mainnet", "Preview", "Preprod"]) + + server.registerTool( + "time_slot_convert", + { + description: + "Convert between Cardano slot numbers and Unix timestamps, or get the current slot. " + + "Actions: 'slotToUnix' converts a slot to a Unix timestamp (ms), " + + "'unixToSlot' converts a Unix timestamp (ms) to a slot, " + + "'currentSlot' returns the current slot for a network, " + + "'getConfig' returns the slot configuration for a network.", + inputSchema: z.object({ + action: z.enum(["slotToUnix", "unixToSlot", "currentSlot", "getConfig"]), + network: SlotConfigNetworkSchema.optional(), + slot: z.string().optional(), + unixTime: z.string().optional(), + customConfig: z + .object({ + zeroTime: z.string(), + zeroSlot: z.string(), + slotLength: z.number().positive() + }) + .optional() + + }) + }, + async ({ action, network, slot, unixTime, customConfig }) => { + const slotConfig = customConfig + ? { + zeroTime: BigInt(customConfig.zeroTime), + zeroSlot: BigInt(customConfig.zeroSlot), + slotLength: customConfig.slotLength + } + : Evolution.Time.SLOT_CONFIG_NETWORK[network ?? "Mainnet"] + + switch (action) { + case "slotToUnix": { + if (!slot) throw new Error("'slot' is required for slotToUnix") + const unix = Evolution.Time.slotToUnixTime(BigInt(slot), slotConfig) + return toolTextResult({ + slot, + unixTimeMs: unix.toString(), + isoDate: new Date(Number(unix)).toISOString() + }) + } + case "unixToSlot": { + if (!unixTime) throw new Error("'unixTime' is required for unixToSlot") + const s = Evolution.Time.unixTimeToSlot(BigInt(unixTime), slotConfig) + return toolTextResult({ unixTimeMs: unixTime, slot: s.toString() }) + } + case "currentSlot": { + const s = Evolution.Time.getCurrentSlot(network ?? "Mainnet") + return toolTextResult({ + network: network ?? "Mainnet", + currentSlot: s.toString(), + currentUnixTimeMs: Date.now().toString() + }) + } + case "getConfig": { + const cfg = customConfig ? slotConfig : Evolution.Time.SLOT_CONFIG_NETWORK[network ?? "Mainnet"] + return toolTextResult({ + network: customConfig ? "Custom" : (network ?? "Mainnet"), + zeroTime: cfg.zeroTime.toString(), + zeroSlot: cfg.zeroSlot.toString(), + slotLength: cfg.slotLength + }) + } + } + } + ) + + // ── Blueprint tools ───────────────────────────────────────────────────── + + server.registerTool( + "blueprint_parse", + { + description: + "Parse a CIP-57 Plutus blueprint JSON. Returns the preamble, validator list " + + "(with compiled code hashes, datum/redeemer types), and definition count.", + inputSchema: z.object({ + blueprintJson: z.string() + }) + }, + async ({ blueprintJson }) => { + const raw = JSON.parse(blueprintJson) as unknown + const blueprint = Evolution.Schema.decodeUnknownSync(Evolution.Blueprint.PlutusBlueprint)(raw) + + const validators = blueprint.validators.map((v: any) => ({ + title: v.title, + hash: v.hash, + datum: v.datum ?? null, + redeemer: v.redeemer ?? null, + compiledCodeSize: v.compiledCode ? v.compiledCode.length : 0 + })) + + return toolTextResult({ + preamble: blueprint.preamble, + validatorCount: validators.length, + validators, + definitionCount: Object.keys(blueprint.definitions ?? {}).length + }) + } + ) + + server.registerTool( + "blueprint_codegen", + { + description: + "Generate TypeScript code from a CIP-57 Plutus blueprint JSON. Uses @evolution-sdk/evolution Blueprint codegen.", + inputSchema: z.object({ + blueprintJson: z.string(), + config: z + .object({ + optionStyle: z.enum(["NullOr", "Option"]).optional(), + unionStyle: z.enum(["Variant", "TaggedUnion"]).optional(), + emptyConstructorStyle: z.enum(["Literal", "Unit"]).optional(), + includeIndex: z.boolean().optional(), + moduleStrategy: z.enum(["flat", "nested"]).optional(), + indent: z.string().optional() + }) + .optional() + + }) + }, + async ({ blueprintJson, config }) => { + const raw = JSON.parse(blueprintJson) as unknown + const blueprint = Evolution.Schema.decodeUnknownSync(Evolution.Blueprint.PlutusBlueprint)(raw) + const codegenConfig = config + ? Evolution.Blueprint.createCodegenConfig(config as any) + : undefined + const typescript = Evolution.Blueprint.generateTypeScript(blueprint, codegenConfig) + + return toolTextResult({ generatedTypeScript: typescript }) + } + ) + + // ── Message signing tools ─────────────────────────────────────────────── + + server.registerTool( + "message_sign", + { + description: + "Sign arbitrary data with a private key following CIP-8 / CIP-30 message signing. " + + "Returns a COSE_Sign1 signed message. Suitable for devnet/testing use.", + inputSchema: z.object({ + addressHex: z.string(), + payload: z.string(), + privateKeyHex: z.string() + }) + }, + async ({ addressHex, payload, privateKeyHex }) => { + const privateKey = Evolution.PrivateKey.fromHex(privateKeyHex) + const signed = Evolution.MessageSigning.SignData.signData(addressHex, hexToBytes(payload), privateKey) + return toolTextResult({ + signature: bytesToHex(signed.signature), + key: bytesToHex(signed.key) + }) + } + ) + + server.registerTool( + "message_verify", + { + description: + "Verify a CIP-8 / CIP-30 signed message. Returns whether the signature is valid.", + inputSchema: z.object({ + addressHex: z.string(), + keyHash: z.string(), + payload: z.string(), + signedMessage: z.object({ + signature: z.string(), + key: z.string() + }) + }) + }, + async ({ addressHex, keyHash, payload, signedMessage }) => { + const valid = Evolution.MessageSigning.SignData.verifyData( + addressHex, + keyHash, + hexToBytes(payload), + { signature: hexToBytes(signedMessage.signature), key: hexToBytes(signedMessage.key) } + ) + return toolTextResult({ valid }) + } + ) + + // ── Fee validation ────────────────────────────────────────────────────── + + server.registerTool( + "fee_validate", + { + description: + "Validate whether a transaction's fee meets the minimum required fee. " + + "Returns isValid, actualFee, minRequiredFee, txSizeBytes, and difference.", + inputSchema: z.object({ + transactionCborHex: z.string(), + minFeeCoefficient: z.string(), + minFeeConstant: z.string(), + fakeWitnessSetCborHex: z.string().optional() + }) + }, + async ({ transactionCborHex, minFeeCoefficient, minFeeConstant, fakeWitnessSetCborHex }) => { + const transaction = Evolution.Transaction.fromCBORHex(transactionCborHex) + const protocolParams = { + minFeeCoefficient: BigInt(minFeeCoefficient), + minFeeConstant: BigInt(minFeeConstant) + } + const fakeWitnessSet = fakeWitnessSetCborHex + ? Evolution.TransactionWitnessSet.fromCBORHex(fakeWitnessSetCborHex) + : undefined + const result = Evolution.FeeValidation.validateTransactionFee( + transaction, + protocolParams, + fakeWitnessSet + ) + return toolTextResult({ + isValid: result.isValid, + actualFee: result.actualFee.toString(), + minRequiredFee: result.minRequiredFee.toString(), + txSizeBytes: result.txSizeBytes, + difference: result.difference.toString() + }) + } + ) + + // ── Key management tools ──────────────────────────────────────────────── + + server.registerTool( + "key_generate", + { + description: + "Generate cryptographic keys, mnemonics, and derive keys from mnemonics. " + + "WARNING: Generated keys are returned in plaintext — use ONLY for devnet / testing. " + + "Actions: 'generateMnemonic' creates a BIP-39 mnemonic, " + + "'validateMnemonic' checks if a mnemonic is valid, " + + "'fromMnemonicCardano' derives a PrivateKey from a mnemonic via BIP32-Ed25519 (Icarus V2), " + + "'toPublicKey' derives the public key (VKey) from a private key, " + + "'keyHash' computes the Ed25519 key hash (blake2b-224) from a private key.", + inputSchema: z.object({ + action: z.enum(["generateMnemonic", "validateMnemonic", "fromMnemonicCardano", "toPublicKey", "keyHash"]), + mnemonic: z.string().optional(), + mnemonicStrength: z.enum(["128", "160", "192", "224", "256"]).optional(), + account: z.number().int().nonnegative().optional(), + role: z.enum(["0", "2"]).optional(), + index: z.number().int().nonnegative().optional(), + password: z.string().optional(), + privateKeyHex: z.string().optional() + }) + }, + async ({ action, mnemonic, mnemonicStrength, account, role, index, password, privateKeyHex }) => { + switch (action) { + case "generateMnemonic": { + const strength = mnemonicStrength ? (Number(mnemonicStrength) as 128 | 160 | 192 | 224 | 256) : undefined + const words = Evolution.PrivateKey.generateMnemonic(strength) + return toolTextResult({ + mnemonic: words, + wordCount: words.split(" ").length, + strength: strength ?? 256 + }) + } + case "validateMnemonic": { + if (!mnemonic) throw new Error("'mnemonic' is required for validateMnemonic") + return toolTextResult({ + valid: Evolution.PrivateKey.validateMnemonic(mnemonic), + wordCount: mnemonic.split(" ").length + }) + } + case "fromMnemonicCardano": { + if (!mnemonic) throw new Error("'mnemonic' is required for fromMnemonicCardano") + const pk = Evolution.PrivateKey.fromMnemonicCardano(mnemonic, { + account: account ?? 0, + role: role ? (Number(role) as 0 | 2) : 0, + index: index ?? 0, + password + }) + const pub = Evolution.PrivateKey.toPublicKey(pk) + const kh = Evolution.KeyHash.fromPrivateKey(pk) + return toolTextResult({ + privateKeyHex: Evolution.PrivateKey.toHex(pk), + privateKeyBech32: Evolution.PrivateKey.toBech32(pk), + publicKeyHex: bytesToHex(Evolution.VKey.toBytes(pub)), + keyHashHex: Evolution.KeyHash.toHex(kh), + derivationPath: `m/1852'/1815'/${account ?? 0}'/${role ?? 0}/${index ?? 0}` + }) + } + case "toPublicKey": { + if (!privateKeyHex) throw new Error("'privateKeyHex' is required for toPublicKey") + const pk = Evolution.PrivateKey.fromHex(privateKeyHex) + const pub = Evolution.PrivateKey.toPublicKey(pk) + return toolTextResult({ + publicKeyHex: bytesToHex(Evolution.VKey.toBytes(pub)) + }) + } + case "keyHash": { + if (!privateKeyHex) throw new Error("'privateKeyHex' is required for keyHash") + const pk = Evolution.PrivateKey.fromHex(privateKeyHex) + const kh = Evolution.KeyHash.fromPrivateKey(pk) + return toolTextResult({ + keyHashHex: Evolution.KeyHash.toHex(kh), + publicKeyHex: bytesToHex(Evolution.VKey.toBytes(Evolution.PrivateKey.toPublicKey(pk))) + }) + } + } + } + ) + + // ── Native script tools ───────────────────────────────────────────────── + + const NativeScriptVariantSchema: z.ZodType = z.lazy(() => + z.discriminatedUnion("tag", [ + z.object({ tag: z.literal("pubKey"), keyHashHex: z.string() }), + z.object({ tag: z.literal("invalidBefore"), slot: z.string() }), + z.object({ tag: z.literal("invalidHereafter"), slot: z.string() }), + z.object({ tag: z.literal("all"), scripts: z.array(NativeScriptVariantSchema) }), + z.object({ tag: z.literal("any"), scripts: z.array(NativeScriptVariantSchema) }), + z.object({ + tag: z.literal("nOfK"), + required: z.string(), + scripts: z.array(NativeScriptVariantSchema) + }) + ]) + ) + + const buildNativeScript = (spec: any): Evolution.NativeScripts.NativeScript => { + switch (spec.tag) { + case "pubKey": + return Evolution.NativeScripts.makeScriptPubKey(hexToBytes(spec.keyHashHex)) + case "invalidBefore": + return Evolution.NativeScripts.makeInvalidBefore(BigInt(spec.slot)) + case "invalidHereafter": + return Evolution.NativeScripts.makeInvalidHereafter(BigInt(spec.slot)) + case "all": + return Evolution.NativeScripts.makeScriptAll(spec.scripts.map((s: any) => buildNativeScript(s).script)) + case "any": + return Evolution.NativeScripts.makeScriptAny(spec.scripts.map((s: any) => buildNativeScript(s).script)) + case "nOfK": + return Evolution.NativeScripts.makeScriptNOfK( + BigInt(spec.required), + spec.scripts.map((s: any) => buildNativeScript(s).script) + ) + default: + throw new Error(`Unknown native script tag: ${(spec as any).tag}`) + } + } + + const serializeNativeVariant = (v: any): any => { + switch (v._tag) { + case "ScriptPubKey": + return { tag: "pubKey", keyHashHex: bytesToHex(v.keyHash) } + case "InvalidBefore": + return { tag: "invalidBefore", slot: v.slot.toString() } + case "InvalidHereafter": + return { tag: "invalidHereafter", slot: v.slot.toString() } + case "ScriptAll": + return { tag: "all", scripts: v.scripts.map(serializeNativeVariant) } + case "ScriptAny": + return { tag: "any", scripts: v.scripts.map(serializeNativeVariant) } + case "ScriptNOfK": + return { tag: "nOfK", required: v.required.toString(), scripts: v.scripts.map(serializeNativeVariant) } + default: + return v + } + } + + server.registerTool( + "native_script_tools", + { + description: + "Build, parse, and analyze Cardano native scripts. " + + "Actions: 'build' creates a native script from a structured spec, " + + "'parseCbor' decodes a native script from CBOR hex, " + + "'toJson' converts a CBOR-encoded script to cardano-cli JSON format, " + + "'extractKeyHashes' lists all required key hashes, " + + "'countRequiredSigners' returns the minimum number of signers needed.", + inputSchema: z.object({ + action: z.enum(["build", "parseCbor", "toJson", "extractKeyHashes", "countRequiredSigners"]), + spec: NativeScriptVariantSchema.optional(), + cborHex: z.string().optional() + }) + }, + async ({ action, spec, cborHex }) => { + switch (action) { + case "build": { + if (!spec) throw new Error("'spec' is required for build") + const ns = buildNativeScript(spec) + const hex = Evolution.NativeScripts.toCBORHex(ns) + const json = Evolution.NativeScripts.toJSON(ns.script) + return toolTextResult({ + cborHex: hex, + json, + script: serializeNativeVariant(ns.script) + }) + } + case "parseCbor": { + if (!cborHex) throw new Error("'cborHex' is required for parseCbor") + const ns = Evolution.NativeScripts.fromCBORHex(cborHex) + return toolTextResult({ + script: serializeNativeVariant(ns.script), + json: Evolution.NativeScripts.toJSON(ns.script), + cborHex + }) + } + case "toJson": { + if (!cborHex) throw new Error("'cborHex' is required for toJson") + const ns = Evolution.NativeScripts.fromCBORHex(cborHex) + return toolTextResult({ json: Evolution.NativeScripts.toJSON(ns.script) }) + } + case "extractKeyHashes": { + if (!cborHex) throw new Error("'cborHex' is required for extractKeyHashes") + const ns = Evolution.NativeScripts.fromCBORHex(cborHex) + const hashes = Evolution.NativeScripts.extractKeyHashes(ns.script) + return toolTextResult({ + keyHashes: hashes.map((h: Uint8Array) => bytesToHex(h)), + count: hashes.length + }) + } + case "countRequiredSigners": { + if (!cborHex) throw new Error("'cborHex' is required for countRequiredSigners") + const ns = Evolution.NativeScripts.fromCBORHex(cborHex) + return toolTextResult({ + requiredSigners: Evolution.NativeScripts.countRequiredSigners(ns.script) + }) + } + } + } + ) + + // ── UTxO set tools ────────────────────────────────────────────────────── + + const UtxoItemSchema = z.object({ + transactionId: z.string(), + index: z.number().int().nonnegative(), + address: z.string(), + assets: z.record(z.string(), z.string()) + }) + + const parseUtxoItem = (item: any): Evolution.UTxO.UTxO => { + const address = Evolution.Address.fromBech32(item.address) + const assets = Evolution.Assets.fromRecord( + Object.fromEntries( + Object.entries(item.assets as Record).map(([k, v]) => [k, globalThis.BigInt(v)]) + ) + ) + const transactionId = Evolution.TransactionHash.fromHex(item.transactionId) + return new Evolution.UTxO.UTxO({ + transactionId, + index: globalThis.BigInt(item.index), + address, + assets + }) + } + + const serializeUtxoItem = (utxo: Evolution.UTxO.UTxO): any => { + const json = utxo.assets.toJSON() as { lovelace?: string; multiAsset?: Record> } + const assetsRecord: Record = {} + if (json.lovelace) assetsRecord.lovelace = json.lovelace + if (json.multiAsset) { + for (const [policy, names] of Object.entries(json.multiAsset)) { + for (const [name, qty] of Object.entries(names)) { + assetsRecord[`${policy}.${name}`] = qty + } + } + } + return { + transactionId: Evolution.TransactionHash.toHex(utxo.transactionId), + index: Number(utxo.index), + address: Evolution.Address.toBech32(utxo.address), + assets: assetsRecord, + outRef: Evolution.UTxO.toOutRefString(utxo) + } + } + + server.registerTool( + "utxo_tools", + { + description: + "Perform UTxO set operations: create sets, compute union/intersection/difference, filter, " + + "check membership, and get size. Useful for preparing coin selection and transaction building.", + inputSchema: z.object({ + action: z.enum(["create", "union", "intersection", "difference", "size"]), + utxos: z.array(UtxoItemSchema).optional(), + left: z.array(UtxoItemSchema).optional(), + right: z.array(UtxoItemSchema).optional() + }) + }, + async ({ action, utxos, left, right }) => { + switch (action) { + case "create": { + if (!utxos) throw new Error("'utxos' is required for create") + const set = Evolution.UTxO.fromIterable(utxos.map(parseUtxoItem)) + return toolTextResult({ + size: Evolution.UTxO.size(set), + utxos: Evolution.UTxO.toArray(set).map(serializeUtxoItem) + }) + } + case "union": + case "intersection": + case "difference": { + if (!left || !right) throw new Error("'left' and 'right' are required for set operations") + const setA = Evolution.UTxO.fromIterable(left.map(parseUtxoItem)) + const setB = Evolution.UTxO.fromIterable(right.map(parseUtxoItem)) + const result = + action === "union" + ? Evolution.UTxO.union(setA, setB) + : action === "intersection" + ? Evolution.UTxO.intersection(setA, setB) + : Evolution.UTxO.difference(setA, setB) + return toolTextResult({ + operation: action, + leftSize: Evolution.UTxO.size(setA), + rightSize: Evolution.UTxO.size(setB), + resultSize: Evolution.UTxO.size(result), + utxos: Evolution.UTxO.toArray(result).map(serializeUtxoItem) + }) + } + case "size": { + if (!utxos) throw new Error("'utxos' is required for size") + const set = Evolution.UTxO.fromIterable(utxos.map(parseUtxoItem)) + return toolTextResult({ + size: Evolution.UTxO.size(set), + isEmpty: Evolution.UTxO.isEmpty(set) + }) + } + } + } + ) + + // ── Encoding codec (bech32 + bytes) ────────────────────────────────── + + server.registerTool( + "encoding_codec", + { + description: + "Bech32 encode/decode and hex byte conversion/validation/comparison", + inputSchema: z.object({ + action: z.enum(["bech32Encode", "bech32Decode", "bytesFromHex", "bytesValidate", "bytesEquals"]), + bech32: z.string().optional(), + hex: z.string().optional(), + prefix: z.string().optional(), + expectedLength: z.number().int().positive().optional(), + leftHex: z.string().optional(), + rightHex: z.string().optional() + }) + }, + async ({ action, bech32, hex, prefix, expectedLength, leftHex, rightHex }) => { + switch (action) { + case "bech32Encode": { + if (!hex) throw new Error("'hex' is required for bech32Encode") + if (!prefix) throw new Error("'prefix' is required for bech32Encode") + const encoded = Evolution.Schema.decodeSync(Evolution.Bech32.FromHex(prefix))(hex) + return toolTextResult({ bech32: encoded, hex, prefix }) + } + case "bech32Decode": { + if (!bech32) throw new Error("'bech32' is required for bech32Decode") + const sepIdx = bech32.lastIndexOf("1") + if (sepIdx < 1) throw new Error("Invalid bech32 string: no separator found") + const hrp = bech32.substring(0, sepIdx) + const decodedHex = Evolution.Schema.encodeSync(Evolution.Bech32.FromHex(hrp))(bech32) + return toolTextResult({ + prefix: hrp, + hex: decodedHex, + byteLength: decodedHex.length / 2, + bech32 + }) + } + case "bytesFromHex": { + if (!hex) throw new Error("'hex' is required for bytesFromHex") + const bytes = Evolution.Bytes.fromHex(hex) + return toolTextResult({ + hex: Evolution.Bytes.toHex(bytes), + byteLength: bytes.length, + hexLength: hex.length + }) + } + case "bytesValidate": { + if (!hex) throw new Error("'hex' is required for bytesValidate") + const bytes = Evolution.Bytes.fromHex(hex) + const byteLength = bytes.length + const validSizes = [4, 16, 28, 29, 32, 57, 64, 80, 96, 128, 448] + const matchesExpected = expectedLength ? byteLength === expectedLength : true + const matchesKnownSize = validSizes.includes(byteLength) + return toolTextResult({ + hex: Evolution.Bytes.toHex(bytes), + byteLength, + matchesExpected, + matchesKnownSize, + expectedLength: expectedLength ?? null, + knownSizes: validSizes + }) + } + case "bytesEquals": { + if (!leftHex || !rightHex) throw new Error("'leftHex' and 'rightHex' are required for bytesEquals") + const left = Evolution.Bytes.fromHex(leftHex) + const right = Evolution.Bytes.fromHex(rightHex) + return toolTextResult({ + equal: Evolution.Bytes.equals(left, right), + leftLength: left.length, + rightLength: right.length + }) + } + } + } + ) + + // ── Address builder ────────────────────────────────────────────────── + + server.registerTool( + "address_build", + { + description: + "Build Cardano addresses from credential hashes. Supports BaseAddress (payment + stake), " + + "EnterpriseAddress (payment only), and RewardAddress (stake only). " + + "Credentials can be key hashes (28-byte hex, 56 chars) or script hashes. " + + "Returns the address as bech32 and hex.", + inputSchema: z.object({ + type: z.enum(["base", "enterprise", "reward"]), + networkId: z.number().int().min(0).max(1), + paymentHash: z.string().optional(), + paymentType: z.enum(["key", "script"]).optional(), + stakeHash: z.string().optional(), + stakeType: z.enum(["key", "script"]).optional() + }) + }, + async ({ type, networkId, paymentHash, paymentType, stakeHash, stakeType }) => { + const makeCredential = (hash: string, credType: "key" | "script" = "key") => + credType === "key" + ? Evolution.Credential.makeKeyHash(hexToBytes(hash)) + : Evolution.Credential.makeScriptHash(hexToBytes(hash)) + + switch (type) { + case "base": { + if (!paymentHash) throw new Error("'paymentHash' is required for base address") + if (!stakeHash) throw new Error("'stakeHash' is required for base address") + const paymentCredential = makeCredential(paymentHash, paymentType ?? "key") + const stakeCredential = makeCredential(stakeHash, stakeType ?? "key") + const addr = new Evolution.BaseAddress.BaseAddress({ networkId, paymentCredential, stakeCredential }) + const bytes = Evolution.BaseAddress.toBytes(addr) + const eraAddr = Evolution.AddressEras.fromBytes(bytes) + const bech32 = Evolution.AddressEras.toBech32(eraAddr) + return toolTextResult({ + bech32, + hex: Evolution.AddressEras.toHex(eraAddr), + type: "base", + networkId, + paymentCredential: { hash: paymentHash, type: paymentType ?? "key" }, + stakeCredential: { hash: stakeHash, type: stakeType ?? "key" } + }) + } + case "enterprise": { + if (!paymentHash) throw new Error("'paymentHash' is required for enterprise address") + const paymentCredential = makeCredential(paymentHash, paymentType ?? "key") + const addr = new Evolution.EnterpriseAddress.EnterpriseAddress({ networkId, paymentCredential }) + const bytes = Evolution.EnterpriseAddress.toBytes(addr) + const eraAddr = Evolution.AddressEras.fromBytes(bytes) + const bech32 = Evolution.AddressEras.toBech32(eraAddr) + return toolTextResult({ + bech32, + hex: Evolution.AddressEras.toHex(eraAddr), + type: "enterprise", + networkId, + paymentCredential: { hash: paymentHash, type: paymentType ?? "key" } + }) + } + case "reward": { + if (!stakeHash) throw new Error("'stakeHash' is required for reward address") + // Build reward address bytes manually: header byte + 28-byte hash + // Header: 0xe0 = testnet+key, 0xe1 = mainnet+key, 0xf0 = testnet+script, 0xf1 = mainnet+script + const isScript = (stakeType ?? "key") === "script" + const header = (isScript ? 0xf0 : 0xe0) | (networkId & 0x0f) + const hashBytes = hexToBytes(stakeHash) + const addrBytes = new Uint8Array(29) + addrBytes[0] = header + addrBytes.set(hashBytes, 1) + const eraAddr = Evolution.AddressEras.fromBytes(addrBytes) + const bech32 = Evolution.AddressEras.toBech32(eraAddr) + return toolTextResult({ + bech32, + hex: Evolution.AddressEras.toHex(eraAddr), + type: "reward", + networkId, + stakeCredential: { hash: stakeHash, type: stakeType ?? "key" } + }) + } + } + } + ) + + // ── Metadata tools ────────────────────────────────────────────────────── + + server.registerTool( + "metadata_tools", + { + description: + "Build and parse Cardano transaction metadata (TransactionMetadatum). " + + "Actions: 'build' creates metadata from a JSON spec with typed entries, " + + "'parseCbor' decodes metadata from CBOR hex, " + + "'buildAuxiliaryData' creates AuxiliaryData with metadata entries, " + + "'parseAuxiliaryData' decodes AuxiliaryData from CBOR hex.", + inputSchema: z.object({ + action: z.enum(["build", "parseCbor", "buildAuxiliaryData", "parseAuxiliaryData"]), + entries: z + .array( + z.object({ + label: z.string(), + value: z.any() + }) + ) + .optional() + , + cborHex: z.string().optional() + }) + }, + async ({ action, entries, cborHex }) => { + const buildMetadatumValue = (v: unknown): any => { + if (typeof v === "string") return Evolution.TransactionMetadatum.text(v) + if (typeof v === "number") return Evolution.TransactionMetadatum.int(BigInt(v)) + if (Array.isArray(v)) return Evolution.TransactionMetadatum.array(v.map(buildMetadatumValue)) + if (v && typeof v === "object") { + const obj = v as Record + if (obj.type === "bytes" && typeof obj.value === "string") { + return Evolution.TransactionMetadatum.bytes(hexToBytes(obj.value as string)) + } + if (obj.type === "int" && (typeof obj.value === "string" || typeof obj.value === "number")) { + return Evolution.TransactionMetadatum.int(BigInt(obj.value)) + } + if (obj.type === "text" && typeof obj.value === "string") { + return Evolution.TransactionMetadatum.text(obj.value as string) + } + if (obj.type === "list" && Array.isArray(obj.value)) { + return Evolution.TransactionMetadatum.array( + (obj.value as unknown[]).map(buildMetadatumValue) + ) + } + if (obj.type === "map" && Array.isArray(obj.value)) { + const m = new Map( + (obj.value as Array<[unknown, unknown]>).map( + ([k, val]: [unknown, unknown]) => [buildMetadatumValue(k), buildMetadatumValue(val)] as [any, any] + ) + ) + return Evolution.TransactionMetadatum.map(m) + } + // Object treated as key-value map of text keys + const mapEntries = new Map( + Object.entries(obj).map( + ([k, val]) => + [Evolution.TransactionMetadatum.text(k), buildMetadatumValue(val)] as [any, any] + ) + ) + return Evolution.TransactionMetadatum.map(mapEntries) + } + throw new Error(`Unsupported metadata value type: ${typeof v}`) + } + + switch (action) { + case "build": { + if (!entries || entries.length === 0) throw new Error("'entries' is required for build") + const metadatum = Evolution.TransactionMetadatum.fromEntries( + entries.map((e) => [BigInt(e.label), buildMetadatumValue(e.value)] as [bigint, any]) + ) + const hex = Evolution.TransactionMetadatum.toCBORHex(metadatum) + return toolTextResult({ cborHex: hex }) + } + case "parseCbor": { + if (!cborHex) throw new Error("'cborHex' is required for parseCbor") + const metadatum = Evolution.TransactionMetadatum.fromCBORHex(cborHex) + return toolTextResult({ metadatum: toStructured(metadatum), cborHex }) + } + case "buildAuxiliaryData": { + if (!entries || entries.length === 0) throw new Error("'entries' is required for buildAuxiliaryData") + const metadatum = Evolution.TransactionMetadatum.fromEntries( + entries.map((e) => [BigInt(e.label), buildMetadatumValue(e.value)] as [bigint, any]) + ) + const aux = Evolution.AuxiliaryData.conway({ + metadata: metadatum as any, + nativeScripts: [], + plutusV1Scripts: [], + plutusV2Scripts: [], + plutusV3Scripts: [] + }) + const hex = Evolution.AuxiliaryData.toCBORHex(aux) + return toolTextResult({ cborHex: hex, tag: "ConwayAuxiliaryData" }) + } + case "parseAuxiliaryData": { + if (!cborHex) throw new Error("'cborHex' is required for parseAuxiliaryData") + const aux = Evolution.AuxiliaryData.fromCBORHex(cborHex) + return toolTextResult({ auxiliaryData: toStructured(aux), cborHex }) + } + } + } + ) + + // ── Credential tools ──────────────────────────────────────────────────── + + server.registerTool( + "credential_tools", + { + description: + "Build and inspect Cardano credentials. " + + "Actions: 'makeKeyHash' creates a key hash credential from a 28-byte hash hex, " + + "'makeScriptHash' creates a script hash credential, " + + "'fromCbor' decodes a credential from CBOR hex, " + + "'toCbor' encodes a credential to CBOR hex.", + inputSchema: z.object({ + action: z.enum(["makeKeyHash", "makeScriptHash", "fromCbor", "toCbor"]), + hashHex: z.string().optional(), + cborHex: z.string().optional(), + credentialType: z.enum(["key", "script"]).optional() + }) + }, + async ({ action, hashHex, cborHex, credentialType }) => { + switch (action) { + case "makeKeyHash": { + if (!hashHex) throw new Error("'hashHex' is required for makeKeyHash") + const cred = Evolution.Credential.makeKeyHash(hexToBytes(hashHex)) + const hex = Evolution.Credential.toCBORHex(cred) + return toolTextResult({ + credential: { tag: "KeyHash", hash: hashHex }, + cborHex: hex + }) + } + case "makeScriptHash": { + if (!hashHex) throw new Error("'hashHex' is required for makeScriptHash") + const cred = Evolution.Credential.makeScriptHash(hexToBytes(hashHex)) + const hex = Evolution.Credential.toCBORHex(cred) + return toolTextResult({ + credential: { tag: "ScriptHash", hash: hashHex }, + cborHex: hex + }) + } + case "fromCbor": { + if (!cborHex) throw new Error("'cborHex' is required for fromCbor") + const cred = Evolution.Credential.fromCBORHex(cborHex) + return toolTextResult({ + credential: { + tag: cred._tag, + hash: cred.hash + }, + cborHex + }) + } + case "toCbor": { + if (!hashHex) throw new Error("'hashHex' is required for toCbor") + const type = credentialType ?? "key" + const cred = + type === "key" + ? Evolution.Credential.makeKeyHash(hexToBytes(hashHex)) + : Evolution.Credential.makeScriptHash(hexToBytes(hashHex)) + return toolTextResult({ + cborHex: Evolution.Credential.toCBORHex(cred), + credential: { tag: cred._tag, hash: hashHex } + }) + } + } + } + ) + + // ── DRep tools ────────────────────────────────────────────────────────── + + server.registerTool( + "drep_tools", + { + description: + "Build and inspect DRep (Delegated Representative) values for Cardano governance. " + + "Actions: 'fromKeyHash' creates a DRep from a 28-byte key hash, " + + "'fromScriptHash' creates a DRep from a script hash, " + + "'alwaysAbstain' / 'alwaysNoConfidence' create special DRep constants, " + + "'fromBech32' parses a drep1... bech32 string, " + + "'toBech32' converts a DRep hex to bech32, " + + "'fromCbor' decodes a DRep from CBOR hex, " + + "'inspect' returns the hex and bech32 representations of a DRep from its hex encoding.", + inputSchema: z.object({ + action: z.enum([ + "fromKeyHash", + "fromScriptHash", + "alwaysAbstain", + "alwaysNoConfidence", + "fromBech32", + "toBech32", + "fromCbor", + "inspect" + ]), + hashHex: z.string().optional(), + bech32: z.string().optional(), + cborHex: z.string().optional(), + hex: z.string().optional() + }) + }, + async ({ action, hashHex, bech32, cborHex, hex }) => { + const serializeDRep = (d: any) => { + switch (d._tag) { + case "KeyHashDRep": + return { tag: "KeyHashDRep", keyHash: Evolution.KeyHash.toHex(d.keyHash) } + case "ScriptHashDRep": + return { tag: "ScriptHashDRep", scriptHash: d.scriptHash.hash } + case "AlwaysAbstainDRep": + return { tag: "AlwaysAbstainDRep" } + case "AlwaysNoConfidenceDRep": + return { tag: "AlwaysNoConfidenceDRep" } + default: + return d + } + } + + const drepHexAndBech32 = (d: any) => { + // AlwaysAbstain/AlwaysNoConfidence cannot be encoded to hex or bech32 + if (d._tag === "AlwaysAbstainDRep" || d._tag === "AlwaysNoConfidenceDRep") { + return { hex: null, bech32: null } + } + const h = Evolution.DRep.toHex(d) + const b32 = Evolution.DRep.toBech32(d) + return { hex: h, bech32: b32 } + } + + switch (action) { + case "fromKeyHash": { + if (!hashHex) throw new Error("'hashHex' is required for fromKeyHash") + const kh = Evolution.KeyHash.fromHex(hashHex) + const drep = Evolution.DRep.fromKeyHash(kh) + const enc = drepHexAndBech32(drep) + return toolTextResult({ drep: serializeDRep(drep), ...enc }) + } + case "fromScriptHash": { + if (!hashHex) throw new Error("'hashHex' is required for fromScriptHash") + const sh = Evolution.ScriptHash.fromHex(hashHex) + const drep = Evolution.DRep.fromScriptHash(sh) + const enc = drepHexAndBech32(drep) + return toolTextResult({ drep: serializeDRep(drep), ...enc }) + } + case "alwaysAbstain": { + const drep = Evolution.DRep.alwaysAbstain() + return toolTextResult({ drep: serializeDRep(drep) }) + } + case "alwaysNoConfidence": { + const drep = Evolution.DRep.alwaysNoConfidence() + return toolTextResult({ drep: serializeDRep(drep) }) + } + case "fromBech32": { + if (!bech32) throw new Error("'bech32' is required for fromBech32") + const drep = Evolution.Schema.decodeSync(Evolution.DRep.FromBech32)(bech32) + const enc = drepHexAndBech32(drep) + return toolTextResult({ drep: serializeDRep(drep), ...enc }) + } + case "toBech32": { + if (!hex) throw new Error("'hex' is required for toBech32") + const drep = Evolution.Schema.decodeSync(Evolution.DRep.FromHex)(hex) + const b32 = Evolution.DRep.toBech32(drep) + return toolTextResult({ drep: serializeDRep(drep), bech32: b32, hex }) + } + case "fromCbor": { + if (!cborHex) throw new Error("'cborHex' is required for fromCbor") + const drep = Evolution.DRep.fromCBORHex(cborHex) + const enc = drepHexAndBech32(drep) + return toolTextResult({ drep: serializeDRep(drep), cborHex, ...enc }) + } + case "inspect": { + if (!hex) throw new Error("'hex' is required for inspect") + const drep = Evolution.Schema.decodeSync(Evolution.DRep.FromHex)(hex) + const enc = drepHexAndBech32(drep) + return toolTextResult({ drep: serializeDRep(drep), ...enc }) + } + } + } + ) + + // ── Value tools ───────────────────────────────────────────────────────── + + server.registerTool( + "value_tools", + { + description: + "Cardano Value arithmetic and inspection. " + + "Actions: 'onlyCoin' creates an ADA-only Value, " + + "'withAssets' creates a Value with ADA + multi-asset from CBOR hex, " + + "'add' / 'subtract' perform Value arithmetic (CBOR hex inputs), " + + "'geq' checks if first Value >= second, " + + "'getAda' extracts the ADA (lovelace) amount, " + + "'isAdaOnly' checks if Value has only ADA, " + + "'getAssets' extracts the multi-asset map.", + inputSchema: z.object({ + action: z.enum(["onlyCoin", "withAssets", "add", "subtract", "geq", "getAda", "isAdaOnly", "getAssets"]), + lovelace: z.string().optional(), + multiAssetCborHex: z.string().optional(), + valueCborHex: z.string().optional(), + valueCborHexB: z.string().optional() + }) + }, + async ({ action, lovelace, multiAssetCborHex, valueCborHex, valueCborHexB }) => { + switch (action) { + case "onlyCoin": { + if (!lovelace) throw new Error("'lovelace' is required") + const v = Evolution.Value.onlyCoin(BigInt(lovelace)) + const hex = Evolution.Value.toCBORHex(v) + return toolTextResult({ value: { tag: v._tag, coin: lovelace }, cborHex: hex }) + } + case "withAssets": { + if (!lovelace) throw new Error("'lovelace' is required") + if (!multiAssetCborHex) throw new Error("'multiAssetCborHex' is required") + const ma = Evolution.MultiAsset.fromCBORHex(multiAssetCborHex) + const v = Evolution.Value.withAssets(BigInt(lovelace), ma) + const hex = Evolution.Value.toCBORHex(v) + return toolTextResult({ value: { tag: v._tag, coin: lovelace }, cborHex: hex }) + } + case "add": { + if (!valueCborHex) throw new Error("'valueCborHex' is required") + if (!valueCborHexB) throw new Error("'valueCborHexB' is required") + const a = Evolution.Value.fromCBORHex(valueCborHex) + const b = Evolution.Value.fromCBORHex(valueCborHexB) + const result = Evolution.Value.add(a, b) + const hex = Evolution.Value.toCBORHex(result) + return toolTextResult({ value: { tag: result._tag, coin: String(Evolution.Value.getAda(result)) }, cborHex: hex }) + } + case "subtract": { + if (!valueCborHex) throw new Error("'valueCborHex' is required") + if (!valueCborHexB) throw new Error("'valueCborHexB' is required") + const a = Evolution.Value.fromCBORHex(valueCborHex) + const b = Evolution.Value.fromCBORHex(valueCborHexB) + const result = Evolution.Value.subtract(a, b) + const hex = Evolution.Value.toCBORHex(result) + return toolTextResult({ value: { tag: result._tag, coin: String(Evolution.Value.getAda(result)) }, cborHex: hex }) + } + case "geq": { + if (!valueCborHex) throw new Error("'valueCborHex' is required") + if (!valueCborHexB) throw new Error("'valueCborHexB' is required") + const a = Evolution.Value.fromCBORHex(valueCborHex) + const b = Evolution.Value.fromCBORHex(valueCborHexB) + return toolTextResult({ geq: Evolution.Value.geq(a, b) }) + } + case "getAda": { + if (!valueCborHex) throw new Error("'valueCborHex' is required") + const v = Evolution.Value.fromCBORHex(valueCborHex) + return toolTextResult({ lovelace: String(Evolution.Value.getAda(v)) }) + } + case "isAdaOnly": { + if (!valueCborHex) throw new Error("'valueCborHex' is required") + const v = Evolution.Value.fromCBORHex(valueCborHex) + return toolTextResult({ isAdaOnly: Evolution.Value.isAdaOnly(v) }) + } + case "getAssets": { + if (!valueCborHex) throw new Error("'valueCborHex' is required") + const v = Evolution.Value.fromCBORHex(valueCborHex) + const hasMA = Evolution.Value.hasAssets(v) + if (!hasMA) return toolTextResult({ hasAssets: false, multiAssetCborHex: null }) + const ma = (v as any).assets + const maHex = Evolution.MultiAsset.toCBORHex(ma) + return toolTextResult({ hasAssets: true, multiAssetCborHex: maHex }) + } + } + } + ) + + // ── Assets tools ──────────────────────────────────────────────────────── + + server.registerTool( + "assets_tools", + { + description: + "Cardano Assets construction, arithmetic, and inspection. " + + "Actions: 'fromLovelace' creates ADA-only assets, " + + "'fromAsset' creates assets with a single token, " + + "'fromHexStrings' creates from hex policy+name, " + + "'fromRecord' creates from a JSON record, " + + "'merge' combines two Assets (sums all quantities), " + + "'subtract' subtracts one from another, " + + "'lovelaceOf' extracts ADA, " + + "'getUnits' lists all unit strings, " + + "'covers' checks if first Assets covers second, " + + "'flatten' lists all (policyHex, nameHex, qty) triples.", + inputSchema: z.object({ + action: z.enum([ + "fromLovelace", "fromAsset", "fromHexStrings", "fromRecord", + "merge", "subtract", "lovelaceOf", "getUnits", "covers", + "flatten", "hasMultiAsset", "policies", "addLovelace", + "quantityOf", "toCbor", "fromCbor" + ]), + lovelace: z.string().optional(), + policyIdHex: z.string().optional(), + assetNameHex: z.string().optional(), + quantity: z.string().optional(), + record: z.record(z.string(), z.string()).optional(), + cborHex: z.string().optional(), + cborHexB: z.string().optional() + }) + }, + async ({ action, lovelace, policyIdHex, assetNameHex, quantity, record, cborHex, cborHexB }) => { + const serializeAssets = (a: Evolution.Assets.Assets) => { + const units = Evolution.Assets.getUnits(a) + const obj: Record = {} + for (const u of units) { + obj[u] = String(Evolution.Assets.getByUnit(a, u)) + } + return obj + } + + switch (action) { + case "fromLovelace": { + if (!lovelace) throw new Error("'lovelace' is required") + const a = Evolution.Assets.fromLovelace(BigInt(lovelace)) + return toolTextResult({ assets: serializeAssets(a), cborHex: Evolution.Assets.toCBORHex(a) }) + } + case "fromAsset": { + if (!policyIdHex || !assetNameHex) throw new Error("'policyIdHex' and 'assetNameHex' are required") + const qty = quantity ? BigInt(quantity) : 1n + const lv = lovelace ? BigInt(lovelace) : 0n + const a = Evolution.Assets.fromHexStrings(policyIdHex, assetNameHex, qty, lv) + return toolTextResult({ assets: serializeAssets(a), cborHex: Evolution.Assets.toCBORHex(a) }) + } + case "fromHexStrings": { + if (!policyIdHex || !assetNameHex) throw new Error("'policyIdHex' and 'assetNameHex' are required") + const qty = quantity ? BigInt(quantity) : 1n + const lv = lovelace ? BigInt(lovelace) : 0n + const a = Evolution.Assets.fromHexStrings(policyIdHex, assetNameHex, qty, lv) + return toolTextResult({ assets: serializeAssets(a), cborHex: Evolution.Assets.toCBORHex(a) }) + } + case "fromRecord": { + if (!record) throw new Error("'record' is required") + const rec: Record = {} + for (const [k, v] of Object.entries(record)) rec[k] = BigInt(v) + const a = Evolution.Assets.fromRecord(rec) + return toolTextResult({ assets: serializeAssets(a), cborHex: Evolution.Assets.toCBORHex(a) }) + } + case "merge": { + if (!cborHex || !cborHexB) throw new Error("'cborHex' and 'cborHexB' are required") + const a = Evolution.Assets.fromCBORHex(cborHex) + const b = Evolution.Assets.fromCBORHex(cborHexB) + const merged = Evolution.Assets.merge(a, b) + return toolTextResult({ assets: serializeAssets(merged), cborHex: Evolution.Assets.toCBORHex(merged) }) + } + case "subtract": { + if (!cborHex || !cborHexB) throw new Error("'cborHex' and 'cborHexB' are required") + const a = Evolution.Assets.fromCBORHex(cborHex) + const b = Evolution.Assets.fromCBORHex(cborHexB) + const result = Evolution.Assets.subtract(a, b) + return toolTextResult({ assets: serializeAssets(result), cborHex: Evolution.Assets.toCBORHex(result) }) + } + case "lovelaceOf": { + if (!cborHex) throw new Error("'cborHex' is required") + const a = Evolution.Assets.fromCBORHex(cborHex) + return toolTextResult({ lovelace: String(Evolution.Assets.lovelaceOf(a)) }) + } + case "getUnits": { + if (!cborHex) throw new Error("'cborHex' is required") + const a = Evolution.Assets.fromCBORHex(cborHex) + return toolTextResult({ units: Evolution.Assets.getUnits(a) }) + } + case "covers": { + if (!cborHex || !cborHexB) throw new Error("'cborHex' and 'cborHexB' are required") + const a = Evolution.Assets.fromCBORHex(cborHex) + const b = Evolution.Assets.fromCBORHex(cborHexB) + return toolTextResult({ covers: Evolution.Assets.covers(a, b) }) + } + case "flatten": { + if (!cborHex) throw new Error("'cborHex' is required") + const a = Evolution.Assets.fromCBORHex(cborHex) + const flat = Evolution.Assets.flatten(a) + const entries = flat.map(([p, n, q]: [any, any, any]) => ({ + policyIdHex: bytesToHex(p.hash ?? p), + assetNameHex: bytesToHex(n.bytes ?? n), + quantity: String(q) + })) + return toolTextResult({ entries }) + } + case "hasMultiAsset": { + if (!cborHex) throw new Error("'cborHex' is required") + const a = Evolution.Assets.fromCBORHex(cborHex) + return toolTextResult({ hasMultiAsset: Evolution.Assets.hasMultiAsset(a) }) + } + case "policies": { + if (!cborHex) throw new Error("'cborHex' is required") + const a = Evolution.Assets.fromCBORHex(cborHex) + const pols = Evolution.Assets.policies(a) + return toolTextResult({ policies: pols.map((p: any) => bytesToHex(p.hash ?? p)) }) + } + case "addLovelace": { + if (!cborHex || !lovelace) throw new Error("'cborHex' and 'lovelace' are required") + const a = Evolution.Assets.fromCBORHex(cborHex) + const result = Evolution.Assets.addLovelace(a, BigInt(lovelace)) + return toolTextResult({ assets: serializeAssets(result), cborHex: Evolution.Assets.toCBORHex(result) }) + } + case "quantityOf": { + if (!cborHex || !policyIdHex || !assetNameHex) throw new Error("'cborHex', 'policyIdHex', and 'assetNameHex' are required") + const a = Evolution.Assets.fromCBORHex(cborHex) + const qty = Evolution.Assets.getByUnit(a, policyIdHex + assetNameHex) + return toolTextResult({ quantity: String(qty) }) + } + case "toCbor": { + if (!record && !cborHex) throw new Error("'record' or 'cborHex' is required") + if (record) { + const rec: Record = {} + for (const [k, v] of Object.entries(record)) rec[k] = BigInt(v) + const a = Evolution.Assets.fromRecord(rec) + return toolTextResult({ cborHex: Evolution.Assets.toCBORHex(a) }) + } + return toolTextResult({ cborHex }) + } + case "fromCbor": { + if (!cborHex) throw new Error("'cborHex' is required") + const a = Evolution.Assets.fromCBORHex(cborHex) + return toolTextResult({ assets: serializeAssets(a) }) + } + } + } + ) + + // ── Unit & Label tools ────────────────────────────────────────────────── + + server.registerTool( + "unit_tools", + { + description: + "CIP-67 asset unit string parsing and construction. " + + "Actions: 'fromUnit' parses a unit string (policyHex+assetNameHex) into policyId, assetName, and optional CIP-67 label; " + + "'toUnit' constructs a unit string from policyId hex, optional name, and optional label; " + + "'toLabel' encodes a CIP-67 label number (0-65535) to its 8-char hex prefix; " + + "'fromLabel' decodes a CIP-67 label hex prefix back to its number.", + inputSchema: z.object({ + action: z.enum(["fromUnit", "toUnit", "toLabel", "fromLabel"]), + unit: z.string().optional(), + policyIdHex: z.string().optional(), + assetNameHex: z.string().optional(), + label: z.number().int().optional(), + labelHex: z.string().optional() + }) + }, + async ({ action, unit, policyIdHex, assetNameHex, label, labelHex }) => { + switch (action) { + case "fromUnit": { + if (!unit) throw new Error("'unit' is required") + const details = AssetUnit.fromUnit(unit) + return toolTextResult({ + policyIdHex: bytesToHex(details.policyId.hash as any), + assetNameHex: details.assetName ? bytesToHex(details.assetName.bytes as any) : null, + nameHex: details.name ? bytesToHex(details.name.bytes as any) : null, + label: details.label + }) + } + case "toUnit": { + if (!policyIdHex) throw new Error("'policyIdHex' is required") + const result = AssetUnit.toUnit( + hexToBytes(policyIdHex) as any, + assetNameHex ? hexToBytes(assetNameHex) as any : null, + label ?? null + ) + return toolTextResult({ unit: result }) + } + case "toLabel": { + if (label === undefined || label === null) throw new Error("'label' is required") + const hex = AssetLabel.toLabel(label) + return toolTextResult({ labelHex: hex, label }) + } + case "fromLabel": { + if (!labelHex) throw new Error("'labelHex' is required") + const num = AssetLabel.fromLabel(labelHex) + return toolTextResult({ label: num ?? null, labelHex }) + } + } + } + ) + + // ── Coin tools ────────────────────────────────────────────────────────── + + server.registerTool( + "coin_tools", + { + description: + "Safe Cardano ADA (Coin) arithmetic with overflow/underflow checking. " + + "Actions: 'add' adds two coin amounts, 'subtract' subtracts (throws on underflow), " + + "'compare' returns -1/0/1, 'validate' checks if a value is a valid Coin (0 to 2^64-1), " + + "'maxCoinValue' returns the maximum valid coin value.", + inputSchema: z.object({ + action: z.enum(["add", "subtract", "compare", "validate", "maxCoinValue"]), + a: z.string().optional(), + b: z.string().optional() + }) + }, + async ({ action, a, b }) => { + switch (action) { + case "add": { + if (!a || !b) throw new Error("'a' and 'b' are required") + const result = Evolution.Coin.add(BigInt(a) as any, BigInt(b) as any) + return toolTextResult({ result: String(result) }) + } + case "subtract": { + if (!a || !b) throw new Error("'a' and 'b' are required") + const result = Evolution.Coin.subtract(BigInt(a) as any, BigInt(b) as any) + return toolTextResult({ result: String(result) }) + } + case "compare": { + if (!a || !b) throw new Error("'a' and 'b' are required") + return toolTextResult({ result: Evolution.Coin.compare(BigInt(a) as any, BigInt(b) as any) }) + } + case "validate": { + if (!a) throw new Error("'a' is required") + return toolTextResult({ valid: Evolution.Coin.is(BigInt(a)) }) + } + case "maxCoinValue": { + return toolTextResult({ maxCoinValue: String(Evolution.Coin.MAX_COIN_VALUE) }) + } + } + } + ) + + // ── Network tools ─────────────────────────────────────────────────────── + + server.registerTool( + "network_tools", + { + description: + "Cardano network name ↔ ID conversion. " + + "'toId' converts a network name (Mainnet/Preview/Preprod) to its numeric ID, " + + "'fromId' converts a numeric network ID back to a name (0→Preview, 1→Mainnet), " + + "'validate' checks if a string is a valid network name.", + inputSchema: z.object({ + action: z.enum(["toId", "fromId", "validate"]), + network: z.string().optional(), + networkId: z.number().int().optional() + }) + }, + async ({ action, network, networkId }) => { + switch (action) { + case "toId": { + if (!network) throw new Error("'network' is required") + const id = Evolution.Network.toId(network as any) + return toolTextResult({ network, networkId: id }) + } + case "fromId": { + if (networkId === undefined || networkId === null) throw new Error("'networkId' is required") + const name = Evolution.Network.fromId(networkId as any) + return toolTextResult({ network: name, networkId }) + } + case "validate": { + if (!network) throw new Error("'network' is required") + return toolTextResult({ valid: Evolution.Network.is(network) }) + } + } + } + ) + + // ── Data construction tools ───────────────────────────────────────────── + + server.registerTool( + "data_construct", + { + description: + "Construct and inspect Plutus Data values programmatically. " + + "Actions: 'constr' builds a constructor with index + fields, " + + "'int' wraps a bigint, 'bytes' wraps a hex string as byte array, " + + "'list' wraps an array of Data (as CBOR hex items), " + + "'map' wraps key-value pairs (each as CBOR hex), " + + "'match' pattern-matches a Data CBOR hex and returns its structure, " + + "'isConstr'/'isMap'/'isList'/'isInt'/'isBytes' type-check a Data CBOR hex.", + inputSchema: z.object({ + action: z.enum(["constr", "int", "bytes", "list", "map", "match", "isConstr", "isMap", "isList", "isInt", "isBytes"]), + index: z.string().optional(), + fieldsCborHex: z.array(z.string()).optional(), + value: z.string().optional(), + entriesCborHex: z.array(z.object({ key: z.string(), value: z.string() })).optional() + , + dataCborHex: z.string().optional() + }) + }, + async ({ action, index, fieldsCborHex, value, entriesCborHex, dataCborHex }) => { + const dataToHex = (d: any) => Evolution.Data.toCBORHex(d) + + switch (action) { + case "constr": { + if (index === undefined) throw new Error("'index' is required") + const fields = (fieldsCborHex ?? []).map((h: string) => Evolution.Data.fromCBORHex(h)) + const c = Evolution.Data.constr(BigInt(index), fields) + return toolTextResult({ cborHex: dataToHex(c), index, fieldCount: fields.length }) + } + case "int": { + if (!value) throw new Error("'value' is required") + const d = Evolution.Data.int(BigInt(value)) + return toolTextResult({ cborHex: dataToHex(d), value }) + } + case "bytes": { + if (!value) throw new Error("'value' (hex string) is required") + const d = Evolution.Data.bytearray(value) + return toolTextResult({ cborHex: dataToHex(d), hex: value }) + } + case "list": { + const items = (fieldsCborHex ?? []).map((h: string) => Evolution.Data.fromCBORHex(h)) + const d = Evolution.Data.list(items) + return toolTextResult({ cborHex: dataToHex(d), length: items.length }) + } + case "map": { + const entries = (entriesCborHex ?? []).map((e: { key: string; value: string }) => [ + Evolution.Data.fromCBORHex(e.key), + Evolution.Data.fromCBORHex(e.value) + ] as [any, any]) + const d = Evolution.Data.map(entries) + return toolTextResult({ cborHex: dataToHex(d), entryCount: entries.length }) + } + case "match": { + if (!dataCborHex) throw new Error("'dataCborHex' is required") + const d = Evolution.Data.fromCBORHex(dataCborHex) + const result = (Evolution.Data.matchData as any)(d, { + Constr: (c: any) => ({ + type: "constr" as const, + index: String(c.index), + fieldsCborHex: (c.fields as any[]).map(dataToHex) + }), + Map: (entries: any) => ({ + type: "map" as const, + entries: [...entries].map(([k, v]: [any, any]) => ({ key: dataToHex(k), value: dataToHex(v) })) + }), + List: (items: any) => ({ + type: "list" as const, + itemsCborHex: (items as any[]).map(dataToHex) + }), + Int: (i: any) => ({ type: "int" as const, value: String(i) }), + Bytes: (b: any) => ({ type: "bytes" as const, hex: bytesToHex(b) }) + }) + return toolTextResult(result as any) + } + case "isConstr": { + if (!dataCborHex) throw new Error("'dataCborHex' is required") + const d = Evolution.Data.fromCBORHex(dataCborHex) + return toolTextResult({ isConstr: Evolution.Data.isConstr(d) }) + } + case "isMap": { + if (!dataCborHex) throw new Error("'dataCborHex' is required") + const d = Evolution.Data.fromCBORHex(dataCborHex) + return toolTextResult({ isMap: Evolution.Data.isMap(d) }) + } + case "isList": { + if (!dataCborHex) throw new Error("'dataCborHex' is required") + const d = Evolution.Data.fromCBORHex(dataCborHex) + return toolTextResult({ isList: Evolution.Data.isList(d) }) + } + case "isInt": { + if (!dataCborHex) throw new Error("'dataCborHex' is required") + const d = Evolution.Data.fromCBORHex(dataCborHex) + return toolTextResult({ isInt: Evolution.Data.isInt(d) }) + } + case "isBytes": { + if (!dataCborHex) throw new Error("'dataCborHex' is required") + const d = Evolution.Data.fromCBORHex(dataCborHex) + return toolTextResult({ isBytes: Evolution.Data.isBytes(d) }) + } + } + } + ) + + // ── Hash tools ────────────────────────────────────────────────────────── + + server.registerTool( + "hash_tools", + { + description: + "Cardano hashing utilities (blake2b-256). " + + "Actions: 'hashTransaction' computes transaction hash from TransactionBody CBOR hex, " + + "'hashTransactionRaw' hashes raw CBOR bytes directly, " + + "'hashAuxiliaryData' hashes AuxiliaryData CBOR hex.", + inputSchema: z.object({ + action: z.enum(["hashTransaction", "hashTransactionRaw", "hashAuxiliaryData"]), + cborHex: z.string() + }) + }, + async ({ action, cborHex }) => { + switch (action) { + case "hashTransaction": { + const body = Evolution.TransactionBody.fromCBORHex(cborHex) + const hash = EvolutionHash.hashTransaction(body) + return toolTextResult({ transactionHash: Evolution.TransactionHash.toHex(hash) }) + } + case "hashTransactionRaw": { + const bytes = hexToBytes(cborHex) + const hash = EvolutionHash.hashTransactionRaw(bytes) + return toolTextResult({ transactionHash: Evolution.TransactionHash.toHex(hash) }) + } + case "hashAuxiliaryData": { + const aux = Evolution.AuxiliaryData.fromCBORHex(cborHex) + const hash = EvolutionHash.hashAuxiliaryData(aux) + return toolTextResult({ auxiliaryDataHash: Evolution.AuxiliaryDataHash.toHex(hash) }) + } + } + } + ) + + // ── Devnet cluster tool ────────────────────────────────────────────── + + const DevnetConfigSchema = z + .object({ + clusterName: z.string().optional(), + networkMagic: z.number().int().positive().optional(), + shelleyGenesis: z + .object({ + slotLength: z.number().positive().optional(), + epochLength: z.number().int().positive().optional(), + activeSlotsCoeff: z.number().min(0).max(1).optional() + }) + .optional(), + kupo: z + .object({ + enabled: z.boolean().optional(), + port: z.number().int().positive().optional() + }) + .optional(), + ogmios: z + .object({ + enabled: z.boolean().optional(), + port: z.number().int().positive().optional() + }) + .optional(), + ports: z + .object({ + node: z.number().int().positive().optional(), + submit: z.number().int().positive().optional() + }) + .optional() + }) + .optional() + + const serializeCluster = (cluster: Devnet.Cluster.Cluster) => ({ + networkName: cluster.networkName, + cardanoNode: { id: cluster.cardanoNode.id, name: cluster.cardanoNode.name }, + kupo: cluster.kupo ? { id: cluster.kupo.id, name: cluster.kupo.name } : null, + ogmios: cluster.ogmios ? { id: cluster.ogmios.id, name: cluster.ogmios.name } : null, + slotConfig: Devnet.Cluster.getSlotConfig(cluster) + }) + + server.registerTool( + "devnet", + { + description: "Local Cardano devnet via Docker: create, start, stop, remove, status, exec, genesis UTxOs, epoch, config", + inputSchema: z.object({ + action: z.enum(["create", "start", "stop", "remove", "status", "exec", "genesis_utxos", "query_epoch", "config_defaults"]), + clusterHandle: z.string().optional(), + config: DevnetConfigSchema, + containerName: z.enum(["cardanoNode", "kupo", "ogmios"]).optional(), + command: z.array(z.string()).min(1).optional(), + genesisAction: z.enum(["calculate", "query"]).optional(), + configSection: z.enum(["all", "shelleyGenesis", "alonzoGenesis", "conwayGenesis", "byronGenesis", "kupo", "ogmios", "nodeConfig"]).optional() + }) + }, + async ({ action, clusterHandle, config, containerName, command, genesisAction, configSection }) => { + if (action === "create") { + const mergedConfig: Partial | undefined = config + ? { + ...(config.clusterName ? { clusterName: config.clusterName } : undefined), + ...(config.networkMagic ? { networkMagic: config.networkMagic } : undefined), + ...(config.ports ? { ports: { ...Devnet.Config.DEFAULT_DEVNET_CONFIG.ports, ...config.ports } } : undefined), + ...(config.shelleyGenesis + ? { shelleyGenesis: { ...Devnet.Config.DEFAULT_SHELLEY_GENESIS, ...config.shelleyGenesis } } + : undefined), + ...(config.kupo + ? { kupo: { ...Devnet.Config.DEFAULT_KUPO_CONFIG, ...config.kupo } } + : undefined), + ...(config.ogmios + ? { ogmios: { ...Devnet.Config.DEFAULT_OGMIOS_CONFIG, ...config.ogmios } } + : undefined) + } + : undefined + + const cluster = await Devnet.Cluster.make(mergedConfig) + const handle = sessionStore.createCluster(cluster, cluster.networkName) + return toolTextResult({ clusterHandle: handle, cluster: serializeCluster(cluster) }) + } + + if (action === "config_defaults") { + const sectionName = configSection ?? "all" + if (sectionName === "all") { + return toolTextResult({ + clusterName: Devnet.Config.DEFAULT_DEVNET_CONFIG.clusterName, + networkMagic: Devnet.Config.DEFAULT_DEVNET_CONFIG.networkMagic, + image: Devnet.Config.DEFAULT_DEVNET_CONFIG.image, + ports: Devnet.Config.DEFAULT_DEVNET_CONFIG.ports, + kupo: Devnet.Config.DEFAULT_KUPO_CONFIG, + ogmios: Devnet.Config.DEFAULT_OGMIOS_CONFIG + }) + } + const sections: Record = { + shelleyGenesis: Devnet.Config.DEFAULT_SHELLEY_GENESIS, + alonzoGenesis: Devnet.Config.DEFAULT_ALONZO_GENESIS, + conwayGenesis: Devnet.Config.DEFAULT_CONWAY_GENESIS, + byronGenesis: Devnet.Config.DEFAULT_BYRON_GENESIS, + kupo: Devnet.Config.DEFAULT_KUPO_CONFIG, + ogmios: Devnet.Config.DEFAULT_OGMIOS_CONFIG, + nodeConfig: Devnet.Config.DEFAULT_NODE_CONFIG + } + return toolTextResult({ [sectionName]: toStructured(sections[sectionName]) }) + } + + // All remaining actions require a clusterHandle + if (!clusterHandle) throw new Error("clusterHandle is required") + const session = sessionStore.getCluster(clusterHandle) + const cluster = session.cluster as Devnet.Cluster.Cluster + + switch (action) { + case "start": { + await Devnet.Cluster.start(cluster) + return toolTextResult({ clusterHandle, status: "started", cluster: serializeCluster(cluster) }) + } + case "stop": { + await Devnet.Cluster.stop(cluster) + return toolTextResult({ clusterHandle, status: "stopped" }) + } + case "remove": { + await Devnet.Cluster.remove(cluster) + sessionStore.delete(clusterHandle) + return toolTextResult({ clusterHandle, status: "removed" }) + } + case "status": { + const containers: Array<{ name: string; container: Devnet.Container.Container }> = containerName + ? (() => { + const c = cluster[containerName] + if (!c) throw new Error(`Container ${containerName} is not part of this cluster`) + return [{ name: containerName, container: c }] + })() + : [ + { name: "cardanoNode", container: cluster.cardanoNode }, + ...(cluster.kupo ? [{ name: "kupo", container: cluster.kupo }] : []), + ...(cluster.ogmios ? [{ name: "ogmios", container: cluster.ogmios }] : []) + ] + const statuses = await Promise.all( + containers.map(async ({ name, container }) => { + const info = await Devnet.Container.getStatus(container) + return { name, containerId: container.id, containerName: container.name, running: info?.State?.Running ?? false, status: info?.State?.Status ?? "unknown" } + }) + ) + return toolTextResult({ clusterHandle, containers: statuses }) + } + case "exec": { + if (!containerName) throw new Error("containerName is required for exec") + if (!command) throw new Error("command is required for exec") + const container = cluster[containerName] + if (!container) throw new Error(`Container ${containerName} is not part of this cluster`) + const output = await Devnet.Container.execCommand(container, command) + return toolTextResult({ containerName, command, output }) + } + case "genesis_utxos": { + const gAction = genesisAction ?? "calculate" + const utxos = gAction === "calculate" + ? await Devnet.Genesis.calculateUtxosFromConfig(cluster.shelleyGenesis) + : await Devnet.Genesis.queryUtxos(cluster) + return toolTextResult({ action: gAction, count: utxos.length, utxos: serializeUtxos(utxos as unknown as ReadonlyArray) }) + } + case "query_epoch": { + const epoch = await Devnet.Genesis.queryCurrentEpoch(cluster) + return toolTextResult({ epoch: epoch.toString() }) + } + } + } + ) + + // ── mint_tools ────────────────────────────────────────────────────────── + server.registerTool( + "mint_tools", + { + description: + "Build and inspect Mint values for minting/burning native tokens. " + + "Positive amounts = mint, negative = burn.", + inputSchema: z.object({ + action: z.enum([ + "singleton", + "empty", + "insert", + "get", + "getByHex", + "has", + "isEmpty", + "policyCount", + "removePolicy", + "removeAsset", + "fromEntries", + "toCbor", + "fromCbor" + ]), + policyIdHex: z.string().optional(), + assetNameHex: z.string().optional(), + amount: z.string().optional(), + mintCborHex: z.string().optional(), + entries: z + .array( + z.object({ + policyIdHex: z.string(), + assets: z.array( + z.object({ assetNameHex: z.string(), amount: z.string() }) + ) + }) + ) + .optional() + }) + }, + async ({ action, policyIdHex, assetNameHex, amount, mintCborHex, entries }) => { + const parseMint = () => { + if (!mintCborHex) throw new Error("mintCborHex is required") + return Evolution.Mint.fromCBORHex(mintCborHex) + } + const parsePid = () => { + if (!policyIdHex) throw new Error("policyIdHex is required") + return Evolution.PolicyId.fromHex(policyIdHex) + } + const parseAn = () => Evolution.AssetName.fromHex(assetNameHex ?? "") + + switch (action) { + case "singleton": { + const mint = Evolution.Mint.singleton(parsePid(), parseAn(), BigInt(amount ?? "1")) + return toolTextResult({ cborHex: Evolution.Mint.toCBORHex(mint) }) + } + case "empty": + return toolTextResult({ cborHex: Evolution.Mint.toCBORHex(Evolution.Mint.empty()) }) + case "insert": { + const base = parseMint() + const result = Evolution.Mint.insert(base, parsePid(), parseAn(), BigInt(amount ?? "1")) + return toolTextResult({ cborHex: Evolution.Mint.toCBORHex(result) }) + } + case "get": { + const v = Evolution.Mint.get(parseMint(), parsePid(), parseAn()) + return toolTextResult({ value: v !== undefined ? v.toString() : null }) + } + case "getByHex": { + const v = Evolution.Mint.getByHex(parseMint(), policyIdHex ?? "", assetNameHex ?? "") + return toolTextResult({ value: v !== undefined ? v.toString() : null }) + } + case "has": + return toolTextResult({ has: Evolution.Mint.has(parseMint(), parsePid(), parseAn()) }) + case "isEmpty": + return toolTextResult({ isEmpty: Evolution.Mint.isEmpty(parseMint()) }) + case "policyCount": + return toolTextResult({ count: Evolution.Mint.policyCount(parseMint()) }) + case "removePolicy": { + const r = Evolution.Mint.removePolicy(parseMint(), parsePid()) + return toolTextResult({ cborHex: Evolution.Mint.toCBORHex(r) }) + } + case "removeAsset": { + const r = Evolution.Mint.removeAsset(parseMint(), parsePid(), parseAn()) + return toolTextResult({ cborHex: Evolution.Mint.toCBORHex(r) }) + } + case "fromEntries": { + if (!entries) throw new Error("entries is required") + let mint = Evolution.Mint.empty() + for (const e of entries) { + const pid = Evolution.PolicyId.fromHex(e.policyIdHex) + for (const a of e.assets) { + mint = Evolution.Mint.insert(mint, pid, Evolution.AssetName.fromHex(a.assetNameHex), BigInt(a.amount)) + } + } + return toolTextResult({ cborHex: Evolution.Mint.toCBORHex(mint) }) + } + case "toCbor": + return toolTextResult({ cborHex: Evolution.Mint.toCBORHex(parseMint()) }) + case "fromCbor": { + const m = parseMint() + return toolTextResult({ + isEmpty: Evolution.Mint.isEmpty(m), + policyCount: Evolution.Mint.policyCount(m), + cborHex: Evolution.Mint.toCBORHex(m) + }) + } + default: + throw new Error(`Unknown mint_tools action: ${action}`) + } + } + ) + + // ── withdrawals_tools ─────────────────────────────────────────────────── + server.registerTool( + "withdrawals_tools", + { + description: + "Build and inspect Withdrawals maps for reward claiming. " + + "RewardAccount is given as hex (e.g. e0 + 28-byte stake credential hash).", + inputSchema: z.object({ + action: z.enum([ + "singleton", + "empty", + "add", + "remove", + "get", + "has", + "isEmpty", + "size", + "entries", + "toCbor", + "fromCbor" + ]), + rewardAccountHex: z.string().optional(), + coin: z.string().optional(), + withdrawalsCborHex: z.string().optional() + }) + }, + async ({ action, rewardAccountHex, coin, withdrawalsCborHex }) => { + const parseW = () => { + if (!withdrawalsCborHex) throw new Error("withdrawalsCborHex is required") + return Evolution.Withdrawals.fromCBORHex(withdrawalsCborHex) + } + const parseRA = () => { + if (!rewardAccountHex) throw new Error("rewardAccountHex is required") + return Evolution.RewardAccount.fromHex(rewardAccountHex) + } + + switch (action) { + case "singleton": + return toolTextResult({ + cborHex: Evolution.Withdrawals.toCBORHex( + Evolution.Withdrawals.singleton(parseRA(), BigInt(coin ?? "0")) + ) + }) + case "empty": + return toolTextResult({ + cborHex: Evolution.Withdrawals.toCBORHex(Evolution.Withdrawals.empty()) + }) + case "add": { + const r = Evolution.Withdrawals.add(parseW(), parseRA(), BigInt(coin ?? "0")) + return toolTextResult({ cborHex: Evolution.Withdrawals.toCBORHex(r) }) + } + case "remove": { + const r = Evolution.Withdrawals.remove(parseW(), parseRA()) + return toolTextResult({ cborHex: Evolution.Withdrawals.toCBORHex(r) }) + } + case "get": { + const v = Evolution.Withdrawals.get(parseW(), parseRA()) + return toolTextResult({ coin: v !== undefined ? v.toString() : null }) + } + case "has": + return toolTextResult({ has: Evolution.Withdrawals.has(parseW(), parseRA()) }) + case "isEmpty": + return toolTextResult({ isEmpty: Evolution.Withdrawals.isEmpty(parseW()) }) + case "size": + return toolTextResult({ size: Evolution.Withdrawals.size(parseW()) }) + case "entries": { + const es = Evolution.Withdrawals.entries(parseW()) + return toolTextResult({ + entries: es.map(([ra, c]) => ({ + rewardAccountHex: Evolution.RewardAccount.toHex(ra), + coin: c.toString() + })) + }) + } + case "toCbor": + return toolTextResult({ cborHex: Evolution.Withdrawals.toCBORHex(parseW()) }) + case "fromCbor": { + const w = parseW() + return toolTextResult({ + isEmpty: Evolution.Withdrawals.isEmpty(w), + size: Evolution.Withdrawals.size(w), + cborHex: Evolution.Withdrawals.toCBORHex(w) + }) + } + default: + throw new Error(`Unknown withdrawals_tools action: ${action}`) + } + } + ) + + // ── anchor_tools ──────────────────────────────────────────────────────── + server.registerTool( + "anchor_tools", + { + description: + "Create and parse governance Anchor values (URL + 32-byte data hash). " + + "Used in proposals, DRep registration, and certificates.", + inputSchema: z.object({ + action: z.enum(["create", "toCbor", "fromCbor"]), + url: z.string().optional(), + dataHashHex: z.string().optional(), + anchorCborHex: z.string().optional() + }) + }, + async ({ action, url, dataHashHex, anchorCborHex }) => { + switch (action) { + case "create": { + if (!url) throw new Error("url is required") + if (!dataHashHex) throw new Error("dataHashHex is required") + const urlObj = new Evolution.Url.Url({ href: url }) + const anchor = new Evolution.Anchor.Anchor({ + anchorUrl: urlObj, + anchorDataHash: hexToBytes(dataHashHex) + }) + return toolTextResult({ cborHex: Evolution.Anchor.toCBORHex(anchor) }) + } + case "toCbor": { + if (!anchorCborHex) throw new Error("anchorCborHex is required") + const a = Evolution.Anchor.fromCBORHex(anchorCborHex) + return toolTextResult({ cborHex: Evolution.Anchor.toCBORHex(a) }) + } + case "fromCbor": { + if (!anchorCborHex) throw new Error("anchorCborHex is required") + const a = Evolution.Anchor.fromCBORHex(anchorCborHex) + return toolTextResult({ + url: (a as any).anchorUrl?.href ?? String((a as any).anchorUrl), + dataHashHex: bytesToHex((a as any).anchorDataHash), + cborHex: Evolution.Anchor.toCBORHex(a) + }) + } + default: + throw new Error(`Unknown anchor_tools action: ${action}`) + } + } + ) + + // ── certificate_tools ─────────────────────────────────────────────────── + server.registerTool( + "certificate_tools", + { + description: + "Build Cardano certificates for staking, delegation, governance, and pool operations. " + + "All credential hashes are 28-byte hex. Supports both pre-Conway and Conway-era certificates.", + inputSchema: z.object({ + action: z.enum([ + "stakeRegistration", + "stakeDeregistration", + "stakeDelegation", + "poolRetirement", + "regCert", + "unregCert", + "voteDelegCert", + "stakeVoteDelegCert", + "stakeRegDelegCert", + "voteRegDelegCert", + "toCbor", + "fromCbor" + ]), + credentialType: z.enum(["keyHash", "scriptHash"]).optional(), + credentialHashHex: z.string().optional(), + poolKeyHashHex: z.string().optional(), + epoch: z.string().optional(), + coin: z.string().optional(), + drepType: z.enum(["keyHash", "scriptHash", "alwaysAbstain", "alwaysNoConfidence"]).optional(), + drepHashHex: z.string().optional(), + certCborHex: z.string().optional() + }) + }, + async ({ + action, credentialType, credentialHashHex, poolKeyHashHex, + epoch, coin, drepType, drepHashHex, certCborHex + }) => { + const parseCred = () => { + if (!credentialHashHex) throw new Error("credentialHashHex is required") + return (credentialType ?? "keyHash") === "keyHash" + ? Evolution.Credential.makeKeyHash(hexToBytes(credentialHashHex)) + : Evolution.Credential.makeScriptHash(hexToBytes(credentialHashHex)) + } + const parseDRep = () => { + const dt = drepType ?? "alwaysAbstain" + if (dt === "alwaysAbstain") return new Evolution.DRep.AlwaysAbstainDRep() + if (dt === "alwaysNoConfidence") return new Evolution.DRep.AlwaysNoConfidenceDRep() + if (!drepHashHex) throw new Error("drepHashHex is required for keyHash/scriptHash DRep") + if (dt === "keyHash") return Evolution.DRep.fromKeyHash(Evolution.KeyHash.fromHex(drepHashHex)) + return Evolution.DRep.fromScriptHash(Evolution.ScriptHash.fromHex(drepHashHex)) + } + + switch (action) { + case "stakeRegistration": { + const cert = new Evolution.Certificate.StakeRegistration({ stakeCredential: parseCred() }) + return toolTextResult({ cborHex: Evolution.Certificate.toCBORHex(cert) }) + } + case "stakeDeregistration": { + const cert = new Evolution.Certificate.StakeDeregistration({ stakeCredential: parseCred() }) + return toolTextResult({ cborHex: Evolution.Certificate.toCBORHex(cert) }) + } + case "stakeDelegation": { + if (!poolKeyHashHex) throw new Error("poolKeyHashHex is required") + const cert = new Evolution.Certificate.StakeDelegation({ + stakeCredential: parseCred(), + poolKeyHash: Evolution.PoolKeyHash.fromHex(poolKeyHashHex) + }) + return toolTextResult({ cborHex: Evolution.Certificate.toCBORHex(cert) }) + } + case "poolRetirement": { + if (!poolKeyHashHex) throw new Error("poolKeyHashHex is required") + if (!epoch) throw new Error("epoch is required") + const cert = new Evolution.Certificate.PoolRetirement({ + poolKeyHash: Evolution.PoolKeyHash.fromHex(poolKeyHashHex), + epoch: BigInt(epoch) + }) + return toolTextResult({ cborHex: Evolution.Certificate.toCBORHex(cert) }) + } + case "regCert": { + const cert = new Evolution.Certificate.RegCert({ + stakeCredential: parseCred(), + coin: BigInt(coin ?? "2000000") + }) + return toolTextResult({ cborHex: Evolution.Certificate.toCBORHex(cert) }) + } + case "unregCert": { + const cert = new Evolution.Certificate.UnregCert({ + stakeCredential: parseCred(), + coin: BigInt(coin ?? "2000000") + }) + return toolTextResult({ cborHex: Evolution.Certificate.toCBORHex(cert) }) + } + case "voteDelegCert": { + const cert = new Evolution.Certificate.VoteDelegCert({ + stakeCredential: parseCred(), + drep: parseDRep() + }) + return toolTextResult({ cborHex: Evolution.Certificate.toCBORHex(cert) }) + } + case "stakeVoteDelegCert": { + if (!poolKeyHashHex) throw new Error("poolKeyHashHex is required") + const cert = new Evolution.Certificate.StakeVoteDelegCert({ + stakeCredential: parseCred(), + poolKeyHash: Evolution.PoolKeyHash.fromHex(poolKeyHashHex), + drep: parseDRep() + }) + return toolTextResult({ cborHex: Evolution.Certificate.toCBORHex(cert) }) + } + case "stakeRegDelegCert": { + if (!poolKeyHashHex) throw new Error("poolKeyHashHex is required") + const cert = new Evolution.Certificate.StakeRegDelegCert({ + stakeCredential: parseCred(), + poolKeyHash: Evolution.PoolKeyHash.fromHex(poolKeyHashHex), + coin: BigInt(coin ?? "2000000") + }) + return toolTextResult({ cborHex: Evolution.Certificate.toCBORHex(cert) }) + } + case "voteRegDelegCert": { + const cert = new Evolution.Certificate.VoteRegDelegCert({ + stakeCredential: parseCred(), + drep: parseDRep(), + coin: BigInt(coin ?? "2000000") + }) + return toolTextResult({ cborHex: Evolution.Certificate.toCBORHex(cert) }) + } + case "toCbor": { + if (!certCborHex) throw new Error("certCborHex is required") + const c = Evolution.Certificate.fromCBORHex(certCborHex) + return toolTextResult({ cborHex: Evolution.Certificate.toCBORHex(c) }) + } + case "fromCbor": { + if (!certCborHex) throw new Error("certCborHex is required") + const c = Evolution.Certificate.fromCBORHex(certCborHex) + return toolTextResult({ + tag: (c as any)._tag ?? "unknown", + cborHex: Evolution.Certificate.toCBORHex(c) + }) + } + default: + throw new Error(`Unknown certificate_tools action: ${action}`) + } + } + ) + + // ── redeemer_tools ────────────────────────────────────────────────────── + server.registerTool( + "redeemer_tools", + { + description: + "Build and inspect Redeemers with ExUnits for Plutus script validation. " + + "Tags: spend, mint, cert, reward. Data is PlutusData CBOR hex.", + inputSchema: z.object({ + action: z.enum([ + "spend", + "mint", + "cert", + "reward", + "fromCbor", + "toCbor" + ]), + index: z.string().optional(), + dataCborHex: z.string().optional(), + mem: z.string().optional(), + steps: z.string().optional(), + redeemerCborHex: z.string().optional() + }) + }, + async ({ action, index, dataCborHex, mem, steps, redeemerCborHex }) => { + const parseExUnits = () => + new Evolution.Redeemer.ExUnits({ + mem: BigInt(mem ?? "0"), + steps: BigInt(steps ?? "0") + }) + const parseData = () => { + if (!dataCborHex) throw new Error("dataCborHex is required") + return Evolution.Data.fromCBORHex(dataCborHex) + } + const idx = BigInt(index ?? "0") + + switch (action) { + case "spend": { + const r = Evolution.Redeemer.spend(idx, parseData(), parseExUnits()) + return toolTextResult({ cborHex: Evolution.Redeemer.toCBORHex(r) }) + } + case "mint": { + const r = Evolution.Redeemer.mint(idx, parseData(), parseExUnits()) + return toolTextResult({ cborHex: Evolution.Redeemer.toCBORHex(r) }) + } + case "cert": { + const r = Evolution.Redeemer.cert(idx, parseData(), parseExUnits()) + return toolTextResult({ cborHex: Evolution.Redeemer.toCBORHex(r) }) + } + case "reward": { + const r = Evolution.Redeemer.reward(idx, parseData(), parseExUnits()) + return toolTextResult({ cborHex: Evolution.Redeemer.toCBORHex(r) }) + } + case "fromCbor": { + if (!redeemerCborHex) throw new Error("redeemerCborHex is required") + const r = Evolution.Redeemer.fromCBORHex(redeemerCborHex) + return toolTextResult({ + tag: (r as any).tag ?? "unknown", + index: String((r as any).index ?? 0), + mem: String((r as any).exUnits?.mem ?? 0), + steps: String((r as any).exUnits?.steps ?? 0), + isSpend: Evolution.Redeemer.isSpend(r), + isMint: Evolution.Redeemer.isMint(r), + cborHex: Evolution.Redeemer.toCBORHex(r) + }) + } + case "toCbor": { + if (!redeemerCborHex) throw new Error("redeemerCborHex is required") + const r = Evolution.Redeemer.fromCBORHex(redeemerCborHex) + return toolTextResult({ cborHex: Evolution.Redeemer.toCBORHex(r) }) + } + default: + throw new Error(`Unknown redeemer_tools action: ${action}`) + } + } + ) + + // ── voting_tools ──────────────────────────────────────────────────────── + server.registerTool( + "voting_tools", + { + description: + "Build VotingProcedures for Cardano governance. Construct voters, votes, and " + + "combine them into VotingProcedures for transaction inclusion.", + inputSchema: z.object({ + action: z.enum([ + "singleVote", + "toCbor", + "fromCbor" + ]), + voterType: z.enum(["drep", "stakePool", "constitutionalCommittee"]).optional(), + voterCredentialType: z.enum(["keyHash", "scriptHash"]).optional(), + voterHashHex: z.string().optional(), + drepType: z.enum(["keyHash", "scriptHash", "alwaysAbstain", "alwaysNoConfidence"]).optional(), + drepHashHex: z.string().optional(), + govActionTxHashHex: z.string().optional(), + govActionIndex: z.string().optional(), + vote: z.enum(["yes", "no", "abstain"]).optional(), + anchorUrl: z.string().optional(), + anchorDataHashHex: z.string().optional(), + votingCborHex: z.string().optional() + }) + }, + async ({ + action, voterType, voterCredentialType, voterHashHex, + drepType, drepHashHex, govActionTxHashHex, govActionIndex, + vote, anchorUrl, anchorDataHashHex, votingCborHex + }) => { + const makeVoter = () => { + const vt = voterType ?? "drep" + if (vt === "drep") { + const dt = drepType ?? "keyHash" + let drep: any + if (dt === "alwaysAbstain") drep = new Evolution.DRep.AlwaysAbstainDRep() + else if (dt === "alwaysNoConfidence") drep = new Evolution.DRep.AlwaysNoConfidenceDRep() + else if (dt === "keyHash") { + if (!drepHashHex) throw new Error("drepHashHex required for keyHash DRep voter") + drep = Evolution.DRep.fromKeyHash(Evolution.KeyHash.fromHex(drepHashHex)) + } else { + if (!drepHashHex) throw new Error("drepHashHex required for scriptHash DRep voter") + drep = Evolution.DRep.fromScriptHash(Evolution.ScriptHash.fromHex(drepHashHex)) + } + return new Evolution.VotingProcedures.DRepVoter({ drep }) + } + if (vt === "stakePool") { + if (!voterHashHex) throw new Error("voterHashHex required for stakePool voter") + return new Evolution.VotingProcedures.StakePoolVoter({ + poolKeyHash: Evolution.PoolKeyHash.fromHex(voterHashHex) + }) + } + if (!voterHashHex) throw new Error("voterHashHex required for CC voter") + const cred = (voterCredentialType ?? "keyHash") === "keyHash" + ? Evolution.Credential.makeKeyHash(hexToBytes(voterHashHex)) + : Evolution.Credential.makeScriptHash(hexToBytes(voterHashHex)) + return new Evolution.VotingProcedures.ConstitutionalCommitteeVoter({ credential: cred }) + } + + const makeVote = () => { + const v = vote ?? "yes" + if (v === "yes") return Evolution.VotingProcedures.yes() + if (v === "no") return Evolution.VotingProcedures.no() + return Evolution.VotingProcedures.abstain() + } + + switch (action) { + case "singleVote": { + if (!govActionTxHashHex) throw new Error("govActionTxHashHex is required") + const ga = new Evolution.GovernanceAction.GovActionId({ + transactionId: Evolution.TransactionHash.fromHex(govActionTxHashHex), + govActionIndex: BigInt(govActionIndex ?? "0") + }) + const voteObj = makeVote() + + let anchor: any = null + if (anchorUrl && anchorDataHashHex) { + const urlObj = new Evolution.Url.Url({ href: anchorUrl }) + anchor = new Evolution.Anchor.Anchor({ + anchorUrl: urlObj, + anchorDataHash: hexToBytes(anchorDataHashHex) + }) + } + + const votingProcedure = new Evolution.VotingProcedures.VotingProcedure({ + vote: voteObj, + anchor + }) + const vp = Evolution.VotingProcedures.singleVote(makeVoter(), ga, votingProcedure) + return toolTextResult({ cborHex: Evolution.VotingProcedures.toCBORHex(vp) }) + } + case "toCbor": { + if (!votingCborHex) throw new Error("votingCborHex is required") + const v = Evolution.VotingProcedures.fromCBORHex(votingCborHex) + return toolTextResult({ cborHex: Evolution.VotingProcedures.toCBORHex(v) }) + } + case "fromCbor": { + if (!votingCborHex) throw new Error("votingCborHex is required") + const v = Evolution.VotingProcedures.fromCBORHex(votingCborHex) + return toolTextResult({ cborHex: Evolution.VotingProcedures.toCBORHex(v) }) + } + default: + throw new Error(`Unknown voting_tools action: ${action}`) + } + } + ) + + // ── script_ref_tools ──────────────────────────────────────────────────── + server.registerTool( + "script_ref_tools", + { + description: + "Build and inspect ScriptRef values (CBOR tag-24 wrapped scripts for transaction output references).", + inputSchema: z.object({ + action: z.enum(["fromHex", "toHex", "toCbor", "fromCbor"]), + hex: z.string().optional(), + cborHex: z.string().optional() + }) + }, + async ({ action, hex, cborHex }) => { + switch (action) { + case "fromHex": { + if (!hex) throw new Error("hex is required") + const sr = Evolution.ScriptRef.fromHex(hex) + return toolTextResult({ + hex: Evolution.ScriptRef.toHex(sr), + cborHex: Evolution.ScriptRef.toCBORHex(sr) + }) + } + case "toHex": { + if (!cborHex) throw new Error("cborHex is required") + const sr = Evolution.ScriptRef.fromCBORHex(cborHex) + return toolTextResult({ hex: Evolution.ScriptRef.toHex(sr) }) + } + case "toCbor": { + if (!hex) throw new Error("hex is required") + const sr = Evolution.ScriptRef.fromHex(hex) + return toolTextResult({ cborHex: Evolution.ScriptRef.toCBORHex(sr) }) + } + case "fromCbor": { + if (!cborHex) throw new Error("cborHex is required") + const sr = Evolution.ScriptRef.fromCBORHex(cborHex) + return toolTextResult({ + hex: Evolution.ScriptRef.toHex(sr), + cborHex: Evolution.ScriptRef.toCBORHex(sr) + }) + } + default: + throw new Error(`Unknown script_ref_tools action: ${action}`) + } + } + ) + + // ── governance_action_tools ───────────────────────────────────────────── + server.registerTool( + "governance_action_tools", + { + description: + "Create and inspect Cardano governance actions (CIP-1694). Supports InfoAction, " + + "NoConfidenceAction, ParameterChangeAction, TreasuryWithdrawalsAction, " + + "HardForkInitiationAction, NewConstitutionAction, UpdateCommitteeAction. " + + "Also builds GovActionId references and pattern-matches action types.", + inputSchema: z.object({ + action: z.enum([ + "infoAction", + "noConfidenceAction", + "parameterChangeAction", + "treasuryWithdrawalsAction", + "hardForkInitiationAction", + "newConstitutionAction", + "updateCommitteeAction", + "govActionId", + "inspect", + "toCbor", + "fromCbor" + ]), + govActionIdTransactionHashHex: z.string().optional(), + govActionIdIndex: z.number().optional(), + prevGovActionIdTransactionHashHex: z.string().optional(), + prevGovActionIdIndex: z.number().optional(), + protocolParamUpdateCborHex: z.string().optional(), + policyHashHex: z.string().optional(), + withdrawals: z.array(z.object({ + rewardAccountHex: z.string(), + coin: z.string() + })).optional(), + protocolVersionMajor: z.number().optional(), + protocolVersionMinor: z.number().optional(), + anchorUrl: z.string().optional(), + anchorDataHashHex: z.string().optional(), + constitutionScriptHashHex: z.string().optional(), + membersToRemoveHex: z.array(z.string()).optional(), + membersToAdd: z.array(z.object({ + credentialHashHex: z.string(), + epoch: z.number() + })).optional(), + thresholdNumerator: z.number().optional(), + thresholdDenominator: z.number().optional(), + cborHex: z.string().optional() + }) + }, + async (args) => { + const { + action, cborHex, + govActionIdTransactionHashHex, govActionIdIndex, + prevGovActionIdTransactionHashHex, prevGovActionIdIndex, + protocolParamUpdateCborHex, policyHashHex, + withdrawals, protocolVersionMajor, protocolVersionMinor, + anchorUrl, anchorDataHashHex, constitutionScriptHashHex, + membersToRemoveHex, membersToAdd, + thresholdNumerator, thresholdDenominator + } = args + + const buildPrevGovActionId = () => { + if (!prevGovActionIdTransactionHashHex) return null + return new Evolution.GovernanceAction.GovActionId({ + transactionId: Evolution.TransactionHash.fromHex(prevGovActionIdTransactionHashHex), + govActionIndex: BigInt(prevGovActionIdIndex ?? 0) + }) + } + + switch (action) { + case "infoAction": { + const ga = new Evolution.GovernanceAction.InfoAction({}) + return toolTextResult({ cborHex: Evolution.GovernanceAction.toCBORHex(ga), type: "InfoAction" }) + } + case "noConfidenceAction": { + const ga = new Evolution.GovernanceAction.NoConfidenceAction({ + govActionId: buildPrevGovActionId() + }) + return toolTextResult({ cborHex: Evolution.GovernanceAction.toCBORHex(ga), type: "NoConfidenceAction" }) + } + case "parameterChangeAction": { + const ppu = protocolParamUpdateCborHex + ? Evolution.ProtocolParamUpdate.fromCBORHex(protocolParamUpdateCborHex) + : new Evolution.ProtocolParamUpdate.ProtocolParamUpdate({}) + const ga = new Evolution.GovernanceAction.ParameterChangeAction({ + govActionId: buildPrevGovActionId(), + protocolParamUpdate: ppu, + policyHash: policyHashHex ? Evolution.ScriptHash.fromHex(policyHashHex) : null + }) + return toolTextResult({ cborHex: Evolution.GovernanceAction.toCBORHex(ga), type: "ParameterChangeAction" }) + } + case "treasuryWithdrawalsAction": { + const wMap = new Map() + for (const w of (withdrawals ?? [])) { + const raBytes = hexToBytes(w.rewardAccountHex) + const ra = Evolution.AddressEras.fromBytes(raBytes) + wMap.set(ra, BigInt(w.coin)) + } + const ga = new Evolution.GovernanceAction.TreasuryWithdrawalsAction({ + withdrawals: wMap as any, + policyHash: policyHashHex ? Evolution.ScriptHash.fromHex(policyHashHex) : null + }) + return toolTextResult({ cborHex: Evolution.GovernanceAction.toCBORHex(ga), type: "TreasuryWithdrawalsAction" }) + } + case "hardForkInitiationAction": { + const pv = new Evolution.ProtocolVersion.ProtocolVersion({ + major: BigInt(protocolVersionMajor ?? 10), + minor: BigInt(protocolVersionMinor ?? 0) + }) + const ga = new Evolution.GovernanceAction.HardForkInitiationAction({ + govActionId: buildPrevGovActionId(), + protocolVersion: pv + }) + return toolTextResult({ cborHex: Evolution.GovernanceAction.toCBORHex(ga), type: "HardForkInitiationAction" }) + } + case "newConstitutionAction": { + if (!anchorUrl || !anchorDataHashHex) throw new Error("anchorUrl and anchorDataHashHex are required") + const anchor = new Evolution.Anchor.Anchor({ + anchorUrl: new Evolution.Url.Url({ href: anchorUrl }), + anchorDataHash: hexToBytes(anchorDataHashHex) + }) + const constitution = new Evolution.Constitution.Constitution({ + anchor, + scriptHash: constitutionScriptHashHex + ? Evolution.ScriptHash.fromHex(constitutionScriptHashHex) + : null + }) + const ga = new Evolution.GovernanceAction.NewConstitutionAction({ + govActionId: buildPrevGovActionId(), + constitution + }) + return toolTextResult({ cborHex: Evolution.GovernanceAction.toCBORHex(ga), type: "NewConstitutionAction" }) + } + case "updateCommitteeAction": { + const toRemove = (membersToRemoveHex ?? []).map(h => { + const bytes = hexToBytes(h) + return bytes.length === 28 + ? Evolution.Credential.makeKeyHash(bytes) + : Evolution.Credential.makeScriptHash(bytes) + }) + const toAdd = new Map() + for (const m of (membersToAdd ?? [])) { + const bytes = hexToBytes(m.credentialHashHex) + const cred = Evolution.Credential.makeKeyHash(bytes) + toAdd.set(cred, BigInt(m.epoch)) + } + const threshold = new Evolution.UnitInterval.UnitInterval({ + numerator: BigInt(thresholdNumerator ?? 1), + denominator: BigInt(thresholdDenominator ?? 2) + }) + const ga = new Evolution.GovernanceAction.UpdateCommitteeAction({ + govActionId: buildPrevGovActionId(), + membersToRemove: toRemove, + membersToAdd: toAdd, + threshold + }) + return toolTextResult({ cborHex: Evolution.GovernanceAction.toCBORHex(ga), type: "UpdateCommitteeAction" }) + } + case "govActionId": { + if (!govActionIdTransactionHashHex) throw new Error("govActionIdTransactionHashHex is required") + const gaid = new Evolution.GovernanceAction.GovActionId({ + transactionId: Evolution.TransactionHash.fromHex(govActionIdTransactionHashHex), + govActionIndex: BigInt(govActionIdIndex ?? 0) + }) + return toolTextResult({ + transactionHashHex: govActionIdTransactionHashHex, + index: govActionIdIndex ?? 0 + }) + } + case "inspect": { + if (!cborHex) throw new Error("cborHex is required") + const ga = Evolution.GovernanceAction.fromCBORHex(cborHex) + const type = Evolution.GovernanceAction.match(ga, { + InfoAction: () => "InfoAction", + NoConfidenceAction: () => "NoConfidenceAction", + ParameterChangeAction: () => "ParameterChangeAction", + TreasuryWithdrawalsAction: () => "TreasuryWithdrawalsAction", + HardForkInitiationAction: () => "HardForkInitiationAction", + NewConstitutionAction: () => "NewConstitutionAction", + UpdateCommitteeAction: () => "UpdateCommitteeAction" + }) + return toolTextResult({ type, cborHex: Evolution.GovernanceAction.toCBORHex(ga) }) + } + case "toCbor": { + if (!cborHex) throw new Error("cborHex is required") + const ga = Evolution.GovernanceAction.fromCBORHex(cborHex) + return toolTextResult({ cborHex: Evolution.GovernanceAction.toCBORHex(ga) }) + } + case "fromCbor": { + if (!cborHex) throw new Error("cborHex is required") + const ga = Evolution.GovernanceAction.fromCBORHex(cborHex) + const type = Evolution.GovernanceAction.match(ga, { + InfoAction: () => "InfoAction", + NoConfidenceAction: () => "NoConfidenceAction", + ParameterChangeAction: () => "ParameterChangeAction", + TreasuryWithdrawalsAction: () => "TreasuryWithdrawalsAction", + HardForkInitiationAction: () => "HardForkInitiationAction", + NewConstitutionAction: () => "NewConstitutionAction", + UpdateCommitteeAction: () => "UpdateCommitteeAction" + }) + return toolTextResult({ type, cborHex: Evolution.GovernanceAction.toCBORHex(ga) }) + } + default: + throw new Error(`Unknown governance_action_tools action: ${action}`) + } + } + ) + + // ── proposal_tools ────────────────────────────────────────────────────── + server.registerTool( + "proposal_tools", + { + description: + "Build and parse Cardano governance ProposalProcedures (CIP-1694). " + + "Combines a deposit, reward account, governance action, and anchor into a proposal.", + inputSchema: z.object({ + action: z.enum(["create", "toCbor", "fromCbor"]), + deposit: z.string().optional(), + rewardAccountHex: z.string().optional(), + governanceActionCborHex: z.string().optional(), + anchorUrl: z.string().optional(), + anchorDataHashHex: z.string().optional(), + cborHex: z.string().optional() + }) + }, + async ({ action, deposit, rewardAccountHex, governanceActionCborHex, anchorUrl, anchorDataHashHex, cborHex }) => { + switch (action) { + case "create": { + if (!deposit) throw new Error("deposit is required") + if (!rewardAccountHex) throw new Error("rewardAccountHex is required") + if (!governanceActionCborHex) throw new Error("governanceActionCborHex is required") + if (!anchorUrl || !anchorDataHashHex) throw new Error("anchorUrl and anchorDataHashHex are required") + const anchor = new Evolution.Anchor.Anchor({ + anchorUrl: new Evolution.Url.Url({ href: anchorUrl }), + anchorDataHash: hexToBytes(anchorDataHashHex) + }) + const ga = Evolution.GovernanceAction.fromCBORHex(governanceActionCborHex) + const ra = Evolution.AddressEras.fromBytes(hexToBytes(rewardAccountHex)) + const pp = new Evolution.ProposalProcedure.ProposalProcedure({ + deposit: BigInt(deposit), + rewardAccount: ra as any, + governanceAction: ga, + anchor + }) + return toolTextResult({ cborHex: Evolution.ProposalProcedure.toCBORHex(pp) }) + } + case "toCbor": { + if (!cborHex) throw new Error("cborHex is required") + const pp = Evolution.ProposalProcedure.fromCBORHex(cborHex) + return toolTextResult({ cborHex: Evolution.ProposalProcedure.toCBORHex(pp) }) + } + case "fromCbor": { + if (!cborHex) throw new Error("cborHex is required") + const pp = Evolution.ProposalProcedure.fromCBORHex(cborHex) + return toolTextResult({ + deposit: String((pp as any).deposit), + governanceActionCborHex: Evolution.GovernanceAction.toCBORHex((pp as any).governanceAction), + anchorUrl: (pp as any).anchor?.anchorUrl?.href ?? "", + anchorDataHashHex: bytesToHex((pp as any).anchor?.anchorDataHash ?? new Uint8Array()), + cborHex: Evolution.ProposalProcedure.toCBORHex(pp) + }) + } + default: + throw new Error(`Unknown proposal_tools action: ${action}`) + } + } + ) + + // ── tx_output_tools ───────────────────────────────────────────────────── + server.registerTool( + "tx_output_tools", + { + description: + "Build and parse Cardano transaction outputs (Babbage era). " + + "Supports ada-only or multi-asset values, optional datum hash, inline datum, and script reference.", + inputSchema: z.object({ + action: z.enum(["create", "fromCbor", "toCbor"]), + addressBech32: z.string().optional(), + lovelace: z.string().optional(), + datumHashHex: z.string().optional(), + inlineDatumCborHex: z.string().optional(), + scriptRefHex: z.string().optional(), + cborHex: z.string().optional() + }) + }, + async ({ action, addressBech32, lovelace, datumHashHex, inlineDatumCborHex, scriptRefHex, cborHex }) => { + switch (action) { + case "create": { + if (!addressBech32) throw new Error("addressBech32 is required") + if (!lovelace) throw new Error("lovelace is required") + const addr = Evolution.AddressEras.fromBech32(addressBech32) + const value = Evolution.Value.onlyCoin(BigInt(lovelace)) + const opts: Record = { address: addr, amount: value } + + if (datumHashHex) { + const DOSchema = Evolution.DatumOption.DatumOptionSchema + const [DatumHash] = DOSchema.members + opts.datumOption = new (DatumHash as any)({ hash: hexToBytes(datumHashHex) }) + } else if (inlineDatumCborHex) { + const data = Evolution.Data.fromCBORHex(inlineDatumCborHex) + const DOSchema = Evolution.DatumOption.DatumOptionSchema + const [, InlineDatum] = DOSchema.members + opts.datumOption = new (InlineDatum as any)({ data }) + } + + if (scriptRefHex) { + opts.scriptRef = Evolution.ScriptRef.fromHex(scriptRefHex) + } + + const txOut = new Evolution.TransactionOutput.BabbageTransactionOutput(opts as any) + return toolTextResult({ cborHex: Evolution.TransactionOutput.toCBORHex(txOut) }) + } + case "fromCbor": { + if (!cborHex) throw new Error("cborHex is required") + const txOut = Evolution.TransactionOutput.fromCBORHex(cborHex) + const result: Record = { + type: (txOut as any)._tag, + cborHex: Evolution.TransactionOutput.toCBORHex(txOut) + } + if ((txOut as any).datumOption) { + const dOpt = (txOut as any).datumOption + if (Evolution.DatumOption.isDatumHash(dOpt)) { + result.datumHashHex = bytesToHex(dOpt.hash) + } else if (Evolution.DatumOption.isInlineDatum(dOpt)) { + result.inlineDatumCborHex = Evolution.Data.toCBORHex(dOpt.data) + } + } + return toolTextResult(result) + } + case "toCbor": { + if (!cborHex) throw new Error("cborHex is required") + const txOut = Evolution.TransactionOutput.fromCBORHex(cborHex) + return toolTextResult({ cborHex: Evolution.TransactionOutput.toCBORHex(txOut) }) + } + default: + throw new Error(`Unknown tx_output_tools action: ${action}`) + } + } + ) + + // ── plutus_data_codec_tools ───────────────────────────────────────────── + server.registerTool( + "plutus_data_codec_tools", + { + description: + "Encode and decode typed Plutus data using the SDK's Plutus codec system. " + + "Supports OutputReference, Credential, Address, Value (Lovelace/PolicyId), " + + "and CIP-68 metadata codecs. Converts between typed representations and CBOR hex.", + inputSchema: z.object({ + action: z.enum([ + "encodeOutputReference", + "decodeOutputReference", + "encodeCredential", + "decodeCredential", + "encodeAddress", + "decodeAddress", + "encodeLovelace", + "decodeLovelace", + "encodeCip68", + "decodeCip68", + "tokenLabels" + ]), + transactionIdHex: z.string().optional(), + outputIndex: z.number().optional(), + credentialType: z.enum(["VerificationKey", "Script"]).optional(), + credentialHashHex: z.string().optional(), + stakeCredentialType: z.enum(["VerificationKey", "Script"]).optional(), + stakeCredentialHashHex: z.string().optional(), + lovelace: z.string().optional(), + cip68MetadataCborEntries: z.array(z.object({ + keyHex: z.string(), + valueCborHex: z.string() + })).optional(), + cip68Version: z.number().optional(), + cip68ExtraCborHex: z.array(z.string()).optional(), + cborHex: z.string().optional() + }) + }, + async (args) => { + const { + action, transactionIdHex, outputIndex, + credentialType, credentialHashHex, + stakeCredentialType, stakeCredentialHashHex, + lovelace, cborHex, + cip68MetadataCborEntries, cip68Version, cip68ExtraCborHex + } = args + + switch (action) { + case "encodeOutputReference": { + if (!transactionIdHex) throw new Error("transactionIdHex is required") + const codec = Evolution.Plutus.OutputReference.Codec + const result = codec.toCBORHex({ + transaction_id: hexToBytes(transactionIdHex), + output_index: BigInt(outputIndex ?? 0) + } as any) + return toolTextResult({ cborHex: result }) + } + case "decodeOutputReference": { + if (!cborHex) throw new Error("cborHex is required") + const codec = Evolution.Plutus.OutputReference.Codec + const result = codec.fromCBORHex(cborHex) + return toolTextResult({ + transactionIdHex: bytesToHex((result as any).transaction_id), + outputIndex: Number((result as any).output_index) + }) + } + case "encodeCredential": { + if (!credentialHashHex) throw new Error("credentialHashHex is required") + const codec = Evolution.Plutus.Credential.CredentialCodec + const cred = credentialType === "Script" + ? { Script: { hash: hexToBytes(credentialHashHex) } } + : { VerificationKey: { hash: hexToBytes(credentialHashHex) } } + const result = codec.toCBORHex(cred as any) + return toolTextResult({ cborHex: result }) + } + case "decodeCredential": { + if (!cborHex) throw new Error("cborHex is required") + const codec = Evolution.Plutus.Credential.CredentialCodec + const result = codec.fromCBORHex(cborHex) as any + if (result.VerificationKey) { + return toolTextResult({ + type: "VerificationKey", + hashHex: bytesToHex(result.VerificationKey.hash) + }) + } else { + return toolTextResult({ + type: "Script", + hashHex: bytesToHex(result.Script.hash) + }) + } + } + case "encodeAddress": { + if (!credentialHashHex) throw new Error("credentialHashHex is required") + const codec = Evolution.Plutus.Address.Codec + const paymentCred = credentialType === "Script" + ? { Script: { hash: hexToBytes(credentialHashHex) } } + : { VerificationKey: { hash: hexToBytes(credentialHashHex) } } + const addr: any = { + payment_credential: paymentCred, + stake_credential: undefined as any + } + if (stakeCredentialHashHex) { + const stakeCred = stakeCredentialType === "Script" + ? { Script: { hash: hexToBytes(stakeCredentialHashHex) } } + : { VerificationKey: { hash: hexToBytes(stakeCredentialHashHex) } } + addr.stake_credential = { Inline: { credential: stakeCred } } + } + const result = codec.toCBORHex(addr) + return toolTextResult({ cborHex: result }) + } + case "decodeAddress": { + if (!cborHex) throw new Error("cborHex is required") + const codec = Evolution.Plutus.Address.Codec + const result = codec.fromCBORHex(cborHex) as any + const out: Record = {} + if (result.payment_credential?.VerificationKey) { + out.paymentCredentialType = "VerificationKey" + out.paymentCredentialHashHex = bytesToHex(result.payment_credential.VerificationKey.hash) + } else if (result.payment_credential?.Script) { + out.paymentCredentialType = "Script" + out.paymentCredentialHashHex = bytesToHex(result.payment_credential.Script.hash) + } + if (result.stake_credential?.Inline?.credential) { + const sc = result.stake_credential.Inline.credential + if (sc.VerificationKey) { + out.stakeCredentialType = "VerificationKey" + out.stakeCredentialHashHex = bytesToHex(sc.VerificationKey.hash) + } else if (sc.Script) { + out.stakeCredentialType = "Script" + out.stakeCredentialHashHex = bytesToHex(sc.Script.hash) + } + } + return toolTextResult(out) + } + case "encodeLovelace": { + if (!lovelace) throw new Error("lovelace is required") + const codec = Evolution.Plutus.Value.LovelaceCodec + return toolTextResult({ cborHex: codec.toCBORHex(BigInt(lovelace)) }) + } + case "decodeLovelace": { + if (!cborHex) throw new Error("cborHex is required") + const codec = Evolution.Plutus.Value.LovelaceCodec + const result = codec.fromCBORHex(cborHex) + return toolTextResult({ lovelace: String(result) }) + } + case "encodeCip68": { + const codec = Evolution.Plutus.CIP68Metadata.Codec + const metadataMap = new Map() + for (const entry of (cip68MetadataCborEntries ?? [])) { + metadataMap.set( + Evolution.Data.bytearray(entry.keyHex), + Evolution.Data.fromCBORHex(entry.valueCborHex) + ) + } + const extra = (cip68ExtraCborHex ?? []).map(h => Evolution.Data.fromCBORHex(h)) + const datum = { metadata: metadataMap, version: BigInt(cip68Version ?? 1), extra } + return toolTextResult({ cborHex: codec.toCBORHex(datum as any) }) + } + case "decodeCip68": { + if (!cborHex) throw new Error("cborHex is required") + const codec = Evolution.Plutus.CIP68Metadata.Codec + const result = codec.fromCBORHex(cborHex) as any + const entries: Array<{ keyHex: string; valueCborHex: string }> = [] + if (result.metadata instanceof Map) { + for (const [k, v] of result.metadata) { + entries.push({ + keyHex: bytesToHex(k instanceof Uint8Array ? k : new Uint8Array()), + valueCborHex: Evolution.Data.toCBORHex(v) + }) + } + } + return toolTextResult({ + version: Number(result.version), + metadataEntries: entries, + extraCount: result.extra?.length ?? 0 + }) + } + case "tokenLabels": { + return toolTextResult({ + REFERENCE_TOKEN_LABEL: Evolution.Plutus.CIP68Metadata.REFERENCE_TOKEN_LABEL, + NFT_TOKEN_LABEL: Evolution.Plutus.CIP68Metadata.NFT_TOKEN_LABEL, + FT_TOKEN_LABEL: Evolution.Plutus.CIP68Metadata.FT_TOKEN_LABEL, + RFT_TOKEN_LABEL: Evolution.Plutus.CIP68Metadata.RFT_TOKEN_LABEL, + description: "CIP-68 token label prefixes: REFERENCE (100) for reference tokens, NFT (222), FT (333), RFT (444)" + }) + } + default: + throw new Error(`Unknown plutus_data_codec_tools action: ${action}`) + } + } + ) + + // ── pool_params_tools ───────────────────────────────────────────────── + server.tool( + "pool_params_tools", + "Build stake pool params, relays, and pool certificates", + { + action: z.enum([ + "createPoolParams", + "createRelay", + "createPoolMetadata", + "poolRegistration", + "poolRetirement", + "hasMinimumCost", + "hasValidMargin", + "calculatePoolRewards", + "getEffectiveStake", + "toCbor", + "fromCbor" + ]), + operatorHex: z.string().optional(), + vrfKeyHashHex: z.string().optional(), + pledge: z.string().optional(), + cost: z.string().optional(), + marginNumerator: z.string().optional(), + marginDenominator: z.string().optional(), + rewardAccountHex: z.string().optional(), + poolOwnerHexes: z.array(z.string()).optional(), + relays: z.array(z.object({ + type: z.enum(["singleHostAddr", "singleHostName", "multiHostName"]), + port: z.number().optional(), + ipv4: z.string().optional(), + ipv6Hex: z.string().optional(), + dnsName: z.string().optional() + })).optional(), + metadataUrl: z.string().optional(), + metadataHashHex: z.string().optional(), + poolKeyHashHex: z.string().optional(), + epoch: z.string().optional(), + poolParamsCbor: z.string().optional(), + minCost: z.string().optional(), + totalStake: z.string().optional(), + sigma: z.string().optional(), + sigmaDenominator: z.string().optional(), + cborHex: z.string().optional() + }, + async ({ action, operatorHex, vrfKeyHashHex, pledge, cost, + marginNumerator, marginDenominator, rewardAccountHex, + poolOwnerHexes, relays, metadataUrl, metadataHashHex, + poolKeyHashHex, epoch, poolParamsCbor, minCost, + totalStake, cborHex }) => { + switch (action) { + case "createPoolParams": { + const operator = Evolution.PoolKeyHash.fromHex(operatorHex!) + const vrfKeyhash = Evolution.VrfKeyHash.fromHex(vrfKeyHashHex!) + const margin = new Evolution.UnitInterval.UnitInterval({ + numerator: BigInt(marginNumerator ?? "1"), + denominator: BigInt(marginDenominator ?? "100") + }) + const rewardAccount = Evolution.RewardAccount.fromHex(rewardAccountHex!) + const poolOwners = (poolOwnerHexes ?? []).map(h => Evolution.KeyHash.fromHex(h)) + + const builtRelays = (relays ?? []).map(r => { + switch (r.type) { + case "singleHostAddr": { + const args: any = {} + if (r.port != null) args.port = BigInt(r.port) + if (r.ipv4) { + const parts = r.ipv4.split(".").map(Number) + args.ipv4 = Evolution.IPv4.fromBytes(new Uint8Array(parts)) + } + if (r.ipv6Hex) { + args.ipv6 = Evolution.IPv6.fromBytes(hexToBytes(r.ipv6Hex)) + } + return Evolution.Relay.fromSingleHostAddr( + new Evolution.SingleHostAddr.SingleHostAddr(args) + ) + } + case "singleHostName": { + return Evolution.Relay.fromSingleHostName( + new Evolution.SingleHostName.SingleHostName({ + port: r.port != null ? BigInt(r.port) : undefined, + dnsName: r.dnsName as any + } as any) + ) + } + case "multiHostName": { + return Evolution.Relay.fromMultiHostName( + new Evolution.MultiHostName.MultiHostName({ + dnsName: r.dnsName as any + }) + ) + } + } + }) + + let poolMetadata: any = undefined + if (metadataUrl && metadataHashHex) { + const url = new (Evolution.Url as any).Url({ href: metadataUrl }) + poolMetadata = new Evolution.PoolMetadata.PoolMetadata({ + url, + hash: hexToBytes(metadataHashHex) + }) + } + + const pp = new Evolution.PoolParams.PoolParams({ + operator, + vrfKeyhash, + pledge: BigInt(pledge ?? "0"), + cost: BigInt(cost ?? "0"), + margin, + rewardAccount, + poolOwners, + relays: builtRelays, + poolMetadata + } as any) + return toolTextResult({ cbor: pp.toCBORHex(), json: pp.toJSON() }) + } + case "createRelay": { + const defs = relays ?? [] + const results = defs.map(r => { + switch (r.type) { + case "singleHostAddr": { + const args: any = {} + if (r.port != null) args.port = BigInt(r.port) + if (r.ipv4) { + const parts = r.ipv4.split(".").map(Number) + args.ipv4 = Evolution.IPv4.fromBytes(new Uint8Array(parts)) + } + if (r.ipv6Hex) args.ipv6 = Evolution.IPv6.fromBytes(hexToBytes(r.ipv6Hex)) + const sha = new Evolution.SingleHostAddr.SingleHostAddr(args) + return { type: r.type, cbor: sha.toCBORHex(), json: sha.toJSON() } + } + case "singleHostName": { + const shn = new Evolution.SingleHostName.SingleHostName({ + port: r.port != null ? BigInt(r.port) : undefined, + dnsName: r.dnsName as any + } as any) + return { type: r.type, cbor: shn.toCBORHex(), json: shn.toJSON() } + } + case "multiHostName": { + const mhn = new Evolution.MultiHostName.MultiHostName({ dnsName: r.dnsName as any }) + return { type: r.type, cbor: mhn.toCBORHex(), json: mhn.toJSON() } + } + } + }) + return toolTextResult({ relays: results }) + } + case "createPoolMetadata": { + const url = new (Evolution.Url as any).Url({ href: metadataUrl! }) + const pm = new Evolution.PoolMetadata.PoolMetadata({ + url, + hash: hexToBytes(metadataHashHex!) + }) + return toolTextResult({ json: pm.toJSON() }) + } + case "poolRegistration": { + const pp = Evolution.PoolParams.fromHex(poolParamsCbor!) + const cert = new Evolution.Certificate.PoolRegistration({ poolParams: pp }) + return toolTextResult({ tag: cert._tag, json: cert.toJSON() }) + } + case "poolRetirement": { + const pkh = Evolution.PoolKeyHash.fromHex(poolKeyHashHex!) + const cert = new Evolution.Certificate.PoolRetirement({ poolKeyHash: pkh, epoch: BigInt(epoch ?? "0") }) + return toolTextResult({ tag: cert._tag, json: cert.toJSON() }) + } + case "hasMinimumCost": { + const pp = Evolution.PoolParams.fromHex(poolParamsCbor!) + return toolTextResult({ hasMinimumCost: Evolution.PoolParams.hasMinimumCost(pp, BigInt(minCost ?? "0")) }) + } + case "hasValidMargin": { + const pp = Evolution.PoolParams.fromHex(poolParamsCbor!) + return toolTextResult({ hasValidMargin: Evolution.PoolParams.hasValidMargin(pp) }) + } + case "calculatePoolRewards": + case "getEffectiveStake": { + return toolTextResult({ + note: `${action} requires runtime protocol parameters and is better used through tx_build_ops` + }) + } + case "toCbor": { + const pp = Evolution.PoolParams.fromHex(poolParamsCbor!) + return toolTextResult({ cbor: pp.toCBORHex() }) + } + case "fromCbor": { + const pp = Evolution.PoolParams.fromHex(cborHex!) + return toolTextResult({ json: pp.toJSON() }) + } + default: + throw new Error(`Unknown pool_params_tools action: ${action}`) + } + } + ) + + // ── drep_cert_tools ───────────────────────────────────────────────── + server.tool( + "drep_cert_tools", + "Build DRep registration/update/unregistration certs", + { + action: z.enum(["regDrep", "unregDrep", "updateDrep"]), + credentialType: z.enum(["keyhash", "scripthash"]), + credentialHashHex: z.string(), + coin: z.string().optional(), + anchorUrl: z.string().optional(), + anchorDataHashHex: z.string().optional() + }, + async ({ action, credentialType, credentialHashHex, coin, anchorUrl, anchorDataHashHex }) => { + const cred = credentialType === "keyhash" + ? Evolution.Credential.makeKeyHash(hexToBytes(credentialHashHex)) + : Evolution.Credential.makeScriptHash(hexToBytes(credentialHashHex)) + + let anchor: InstanceType | null = null + if (anchorUrl && anchorDataHashHex) { + const url = new (Evolution.Url as any).Url({ href: anchorUrl }) + anchor = new Evolution.Anchor.Anchor({ + anchorUrl: url, + anchorDataHash: hexToBytes(anchorDataHashHex) + }) + } + + switch (action) { + case "regDrep": { + const cert = new Evolution.Certificate.RegDrepCert({ + drepCredential: cred, + coin: BigInt(coin ?? "500000000"), + anchor + }) + return toolTextResult({ tag: cert._tag, json: cert.toJSON() }) + } + case "unregDrep": { + const cert = new Evolution.Certificate.UnregDrepCert({ + drepCredential: cred, + coin: BigInt(coin ?? "500000000") + }) + return toolTextResult({ tag: cert._tag, json: cert.toJSON() }) + } + case "updateDrep": { + const cert = new Evolution.Certificate.UpdateDrepCert({ + drepCredential: cred, + anchor + }) + return toolTextResult({ tag: cert._tag, json: cert.toJSON() }) + } + default: + throw new Error(`Unknown drep_cert_tools action: ${action}`) + } + } + ) + + // ── committee_cert_tools ───────────────────────────────────────────── + server.tool( + "committee_cert_tools", + "Build committee authorization and resignation certs", + { + action: z.enum(["authHot", "resignCold"]), + coldCredentialType: z.enum(["keyhash", "scripthash"]), + coldCredentialHashHex: z.string(), + hotCredentialType: z.enum(["keyhash", "scripthash"]).optional(), + hotCredentialHashHex: z.string().optional(), + anchorUrl: z.string().optional(), + anchorDataHashHex: z.string().optional() + }, + async ({ action, coldCredentialType, coldCredentialHashHex, + hotCredentialType, hotCredentialHashHex, anchorUrl, anchorDataHashHex }) => { + const coldCred = coldCredentialType === "keyhash" + ? Evolution.Credential.makeKeyHash(hexToBytes(coldCredentialHashHex)) + : Evolution.Credential.makeScriptHash(hexToBytes(coldCredentialHashHex)) + + switch (action) { + case "authHot": { + const hotCred = hotCredentialType === "keyhash" + ? Evolution.Credential.makeKeyHash(hexToBytes(hotCredentialHashHex!)) + : Evolution.Credential.makeScriptHash(hexToBytes(hotCredentialHashHex!)) + const cert = new Evolution.Certificate.AuthCommitteeHotCert({ + committeeColdCredential: coldCred, + committeeHotCredential: hotCred + }) + return toolTextResult({ tag: cert._tag, json: cert.toJSON() }) + } + case "resignCold": { + let anchor: InstanceType | null = null + if (anchorUrl && anchorDataHashHex) { + const url = new (Evolution.Url as any).Url({ href: anchorUrl }) + anchor = new Evolution.Anchor.Anchor({ + anchorUrl: url, + anchorDataHash: hexToBytes(anchorDataHashHex) + }) + } + const cert = new Evolution.Certificate.ResignCommitteeColdCert({ + committeeColdCredential: coldCred, + anchor + }) + return toolTextResult({ tag: cert._tag, json: cert.toJSON() }) + } + default: + throw new Error(`Unknown committee_cert_tools action: ${action}`) + } + } + ) + + // ── constitution_tools ───────────────────────────────────────────── + server.tool( + "constitution_tools", + "Build Constitution objects for governance", + { + action: z.enum(["create", "toCbor", "fromCbor"]), + anchorUrl: z.string().optional(), + anchorDataHashHex: z.string().optional(), + scriptHashHex: z.string().optional(), + cborHex: z.string().optional() + }, + async ({ action, anchorUrl, anchorDataHashHex, scriptHashHex, cborHex }) => { + switch (action) { + case "create": { + const url = new (Evolution.Url as any).Url({ href: anchorUrl! }) + const anchor = new Evolution.Anchor.Anchor({ + anchorUrl: url, + anchorDataHash: hexToBytes(anchorDataHashHex!) + }) + const scriptHash = scriptHashHex + ? Evolution.ScriptHash.fromHex(scriptHashHex) + : null + const constitution = new Evolution.Constitution.Constitution({ + anchor, + scriptHash + }) + return toolTextResult({ + cbor: Evolution.Constitution.toCBORHex(constitution), + json: constitution.toJSON() + }) + } + case "toCbor": { + const url = new (Evolution.Url as any).Url({ href: anchorUrl! }) + const anchor = new Evolution.Anchor.Anchor({ + anchorUrl: url, + anchorDataHash: hexToBytes(anchorDataHashHex!) + }) + const scriptHash = scriptHashHex + ? Evolution.ScriptHash.fromHex(scriptHashHex) + : null + const constitution = new Evolution.Constitution.Constitution({ + anchor, + scriptHash + }) + return toolTextResult({ cbor: Evolution.Constitution.toCBORHex(constitution) }) + } + case "fromCbor": { + const constitution = Evolution.Constitution.fromCBORHex(cborHex!) + return toolTextResult({ json: constitution.toJSON() }) + } + default: + throw new Error(`Unknown constitution_tools action: ${action}`) + } + } + ) + + // ── protocol_param_update_tools ───────────────────────────────────── + server.tool( + "protocol_param_update_tools", + "Build ProtocolParamUpdate with all optional fields", + { + action: z.enum(["create", "toCbor", "fromCbor"]), + minfeeA: z.string().optional(), + minfeeB: z.string().optional(), + maxBlockBodySize: z.string().optional(), + maxTxSize: z.string().optional(), + maxBlockHeaderSize: z.string().optional(), + keyDeposit: z.string().optional(), + poolDeposit: z.string().optional(), + maxEpoch: z.string().optional(), + nOpt: z.string().optional(), + poolPledgeInfluenceNum: z.string().optional(), + poolPledgeInfluenceDen: z.string().optional(), + expansionRateNum: z.string().optional(), + expansionRateDen: z.string().optional(), + treasuryGrowthRateNum: z.string().optional(), + treasuryGrowthRateDen: z.string().optional(), + minPoolCost: z.string().optional(), + adaPerUtxoByte: z.string().optional(), + maxTxExMem: z.string().optional(), + maxTxExSteps: z.string().optional(), + maxBlockExMem: z.string().optional(), + maxBlockExSteps: z.string().optional(), + exUnitMemPriceNum: z.string().optional(), + exUnitMemPriceDen: z.string().optional(), + exUnitStepPriceNum: z.string().optional(), + exUnitStepPriceDen: z.string().optional(), + maxValueSize: z.string().optional(), + collateralPercentage: z.string().optional(), + maxCollateralInputs: z.string().optional(), + drepVotingThresholds: z.array(z.object({ + numerator: z.string(), + denominator: z.string() + })).optional(), + poolVotingThresholds: z.array(z.object({ + numerator: z.string(), + denominator: z.string() + })).optional(), + minCommitteeSize: z.string().optional(), + committeeTermLimit: z.string().optional(), + governanceActionValidity: z.string().optional(), + governanceActionDeposit: z.string().optional(), + drepDeposit: z.string().optional(), + drepInactivityPeriod: z.string().optional(), + minfeeRefScriptCoinsPerByteNum: z.string().optional(), + minfeeRefScriptCoinsPerByteDen: z.string().optional(), + cborHex: z.string().optional() + }, + async (args) => { + switch (args.action) { + case "create": + case "toCbor": { + const fields: any = {} + const s2b = (s: string | undefined) => s != null ? BigInt(s) : undefined + + if (args.minfeeA != null) fields.minfeeA = s2b(args.minfeeA) + if (args.minfeeB != null) fields.minfeeB = s2b(args.minfeeB) + if (args.maxBlockBodySize != null) fields.maxBlockBodySize = s2b(args.maxBlockBodySize) + if (args.maxTxSize != null) fields.maxTxSize = s2b(args.maxTxSize) + if (args.maxBlockHeaderSize != null) fields.maxBlockHeaderSize = s2b(args.maxBlockHeaderSize) + if (args.keyDeposit != null) fields.keyDeposit = s2b(args.keyDeposit) + if (args.poolDeposit != null) fields.poolDeposit = s2b(args.poolDeposit) + if (args.maxEpoch != null) fields.maxEpoch = s2b(args.maxEpoch) + if (args.nOpt != null) fields.nOpt = s2b(args.nOpt) + if (args.minPoolCost != null) fields.minPoolCost = s2b(args.minPoolCost) + if (args.adaPerUtxoByte != null) fields.adaPerUtxoByte = s2b(args.adaPerUtxoByte) + if (args.maxValueSize != null) fields.maxValueSize = s2b(args.maxValueSize) + if (args.collateralPercentage != null) fields.collateralPercentage = s2b(args.collateralPercentage) + if (args.maxCollateralInputs != null) fields.maxCollateralInputs = s2b(args.maxCollateralInputs) + if (args.minCommitteeSize != null) fields.minCommitteeSize = s2b(args.minCommitteeSize) + if (args.committeeTermLimit != null) fields.committeeTermLimit = s2b(args.committeeTermLimit) + if (args.governanceActionValidity != null) fields.governanceActionValidity = s2b(args.governanceActionValidity) + if (args.governanceActionDeposit != null) fields.governanceActionDeposit = s2b(args.governanceActionDeposit) + if (args.drepDeposit != null) fields.drepDeposit = s2b(args.drepDeposit) + if (args.drepInactivityPeriod != null) fields.drepInactivityPeriod = s2b(args.drepInactivityPeriod) + + // Ratios using NonnegativeInterval (from Cardano module) + const NI = (Evolution as any).Cardano.NonnegativeInterval.NonnegativeInterval + if (args.poolPledgeInfluenceNum != null && args.poolPledgeInfluenceDen != null) { + fields.poolPledgeInfluence = new NI({ + numerator: BigInt(args.poolPledgeInfluenceNum), + denominator: BigInt(args.poolPledgeInfluenceDen) + }) + } + if (args.expansionRateNum != null && args.expansionRateDen != null) { + fields.expansionRate = new Evolution.UnitInterval.UnitInterval({ + numerator: BigInt(args.expansionRateNum), + denominator: BigInt(args.expansionRateDen) + }) + } + if (args.treasuryGrowthRateNum != null && args.treasuryGrowthRateDen != null) { + fields.treasuryGrowthRate = new Evolution.UnitInterval.UnitInterval({ + numerator: BigInt(args.treasuryGrowthRateNum), + denominator: BigInt(args.treasuryGrowthRateDen) + }) + } + if (args.minfeeRefScriptCoinsPerByteNum != null && args.minfeeRefScriptCoinsPerByteDen != null) { + fields.minfeeRefScriptCoinsPerByte = new NI({ + numerator: BigInt(args.minfeeRefScriptCoinsPerByteNum), + denominator: BigInt(args.minfeeRefScriptCoinsPerByteDen) + }) + } + + // ExUnits + if (args.maxTxExMem != null && args.maxTxExSteps != null) { + fields.maxTxExUnits = new Evolution.ProtocolParamUpdate.ExUnits({ + mem: BigInt(args.maxTxExMem), + steps: BigInt(args.maxTxExSteps) + }) + } + if (args.maxBlockExMem != null && args.maxBlockExSteps != null) { + fields.maxBlockExUnits = new Evolution.ProtocolParamUpdate.ExUnits({ + mem: BigInt(args.maxBlockExMem), + steps: BigInt(args.maxBlockExSteps) + }) + } + + // ExUnitPrices + if (args.exUnitMemPriceNum != null && args.exUnitMemPriceDen != null && + args.exUnitStepPriceNum != null && args.exUnitStepPriceDen != null) { + fields.exUnitPrices = new Evolution.ProtocolParamUpdate.ExUnitPrices({ + memPrice: new NI({ + numerator: BigInt(args.exUnitMemPriceNum), + denominator: BigInt(args.exUnitMemPriceDen) + }), + stepPrice: new NI({ + numerator: BigInt(args.exUnitStepPriceNum), + denominator: BigInt(args.exUnitStepPriceDen) + }) + }) + } + + // Voting thresholds + if (args.drepVotingThresholds && args.drepVotingThresholds.length === 10) { + const uis = args.drepVotingThresholds.map(t => + new Evolution.UnitInterval.UnitInterval({ numerator: BigInt(t.numerator), denominator: BigInt(t.denominator) }) + ) + fields.drepVotingThresholds = new Evolution.ProtocolParamUpdate.DRepVotingThresholds({ + t1: uis[0], t2: uis[1], t3: uis[2], t4: uis[3], t5: uis[4], + t6: uis[5], t7: uis[6], t8: uis[7], t9: uis[8], t10: uis[9] + }) + } + if (args.poolVotingThresholds && args.poolVotingThresholds.length === 5) { + const uis = args.poolVotingThresholds.map(t => + new Evolution.UnitInterval.UnitInterval({ numerator: BigInt(t.numerator), denominator: BigInt(t.denominator) }) + ) + fields.poolVotingThresholds = new Evolution.ProtocolParamUpdate.PoolVotingThresholds({ + t1: uis[0], t2: uis[1], t3: uis[2], t4: uis[3], t5: uis[4] + }) + } + + const ppu = new Evolution.ProtocolParamUpdate.ProtocolParamUpdate(fields) + const cbor = Evolution.ProtocolParamUpdate.toCBORHex(ppu) + return toolTextResult(args.action === "toCbor" ? { cbor } : { cbor, fieldsSet: Object.keys(fields) }) + } + case "fromCbor": { + const ppu = Evolution.ProtocolParamUpdate.fromCBORHex(args.cborHex!) + return toolTextResult({ fields: toStructured(ppu) }) + } + default: + throw new Error(`Unknown protocol_param_update_tools action: ${args.action}`) + } + } + ) + + // ── transaction_input_tools ───────────────────────────────────────── + server.tool( + "transaction_input_tools", + "Build TransactionInput references (txHash + index)", + { + action: z.enum(["build", "inspect", "toCbor", "fromCbor"]), + txHashHex: z.string().optional(), + index: z.number().optional(), + cborHex: z.string().optional() + }, + async ({ action, txHashHex, index, cborHex }) => { + switch (action) { + case "build": { + const txHash = Evolution.TransactionHash.fromHex(txHashHex!) + const txIn = new Evolution.TransactionInput.TransactionInput({ + transactionId: txHash, + index: BigInt(index ?? 0) as any + }) + const cbor = Evolution.TransactionInput.toCBORHex(txIn) + return toolTextResult({ cbor, json: txIn.toJSON() }) + } + case "inspect": + case "fromCbor": { + const txIn = Evolution.TransactionInput.fromCBORHex(cborHex!) + return toolTextResult({ json: txIn.toJSON() }) + } + case "toCbor": { + const txHash = Evolution.TransactionHash.fromHex(txHashHex!) + const txIn = new Evolution.TransactionInput.TransactionInput({ + transactionId: txHash, + index: BigInt(index ?? 0) as any + }) + return toolTextResult({ cbor: Evolution.TransactionInput.toCBORHex(txIn) }) + } + default: + throw new Error(`Unknown transaction_input_tools action: ${action}`) + } + } + ) + + // ── transaction_body_tools ───────────────────────────────────────── + server.tool( + "transaction_body_tools", + "Build TransactionBody with inputs, outputs, fee", + { + action: z.enum(["build", "inspect", "fromCbor"]), + inputs: z.array(z.object({ + txHashHex: z.string(), + index: z.number() + })).optional(), + outputs: z.array(z.object({ + addressBech32: z.string(), + lovelace: z.string(), + datumHashHex: z.string().optional(), + inlineDatumCborHex: z.string().optional() + })).optional(), + fee: z.string().optional(), + ttl: z.string().optional(), + validityIntervalStart: z.string().optional(), + auxiliaryDataHashHex: z.string().optional(), + networkId: z.number().optional(), + totalCollateral: z.string().optional(), + donation: z.string().optional(), + cborHex: z.string().optional() + }, + async ({ action, inputs, outputs, fee, ttl, validityIntervalStart, + auxiliaryDataHashHex, networkId, totalCollateral, donation, cborHex }) => { + switch (action) { + case "build": { + const builtInputs = (inputs ?? []).map(i => { + const txHash = Evolution.TransactionHash.fromHex(i.txHashHex) + return new Evolution.TransactionInput.TransactionInput({ + transactionId: txHash, + index: BigInt(i.index) as any + }) + }) + + const builtOutputs = (outputs ?? []).map(o => { + const addr = Evolution.AddressEras.fromBech32(o.addressBech32) + const amount = Evolution.Value.onlyCoin(BigInt(o.lovelace)) + const fields: any = { address: addr, amount } + if (o.datumHashHex) { + const DatumHash = (Evolution as any).DatumOptionSchema.members[0] + fields.datumOption = new DatumHash({ hash: hexToBytes(o.datumHashHex) }) + } else if (o.inlineDatumCborHex) { + const InlineDatum = (Evolution as any).DatumOptionSchema.members[1] + fields.datumOption = new InlineDatum({ data: Evolution.Data.fromCBORHex(o.inlineDatumCborHex) }) + } + return new Evolution.TransactionOutput.BabbageTransactionOutput(fields) + }) + + const bodyFields: any = { + inputs: builtInputs, + outputs: builtOutputs, + fee: BigInt(fee ?? "0") + } + if (ttl != null) bodyFields.ttl = BigInt(ttl) + if (validityIntervalStart != null) bodyFields.validityIntervalStart = BigInt(validityIntervalStart) + if (auxiliaryDataHashHex) bodyFields.auxiliaryDataHash = Evolution.AuxiliaryDataHash.fromHex(auxiliaryDataHashHex) + if (networkId != null) bodyFields.networkId = networkId + if (totalCollateral != null) bodyFields.totalCollateral = BigInt(totalCollateral) + if (donation != null) bodyFields.donation = BigInt(donation) + + const tb = new Evolution.TransactionBody.TransactionBody(bodyFields) + const cbor = Evolution.TransactionBody.toCBORHex(tb) + return toolTextResult({ cbor, fieldsSummary: { + inputCount: builtInputs.length, + outputCount: builtOutputs.length, + fee: fee ?? "0" + }}) + } + case "inspect": + case "fromCbor": { + const tb = Evolution.TransactionBody.fromCBORHex(cborHex!) + return toolTextResult({ json: tb.toJSON() }) + } + default: + throw new Error(`Unknown transaction_body_tools action: ${action}`) + } + } + ) + + // ── pointer_address_tools ───────────────────────────────────────── + server.tool( + "pointer_address_tools", + "Build PointerAddress (slot-based stake reference)", + { + action: z.enum(["buildPointer", "buildAddress", "inspect"]), + slot: z.number().optional(), + txIndex: z.number().optional(), + certIndex: z.number().optional(), + networkId: z.number().optional(), + paymentCredentialType: z.enum(["keyhash", "scripthash"]).optional(), + paymentCredentialHashHex: z.string().optional(), + hex: z.string().optional() + }, + async ({ action, slot, txIndex, certIndex, networkId, + paymentCredentialType, paymentCredentialHashHex, hex }) => { + switch (action) { + case "buildPointer": { + const ptr = new (Evolution as any).Pointer.Pointer({ + slot: slot ?? 1, + txIndex: txIndex ?? 1, + certIndex: certIndex ?? 1 + }) + return toolTextResult({ json: ptr.toJSON() }) + } + case "buildAddress": { + const ptr = new (Evolution as any).Pointer.Pointer({ + slot: slot ?? 1, + txIndex: txIndex ?? 1, + certIndex: certIndex ?? 1 + }) + const cred = paymentCredentialType === "scripthash" + ? Evolution.Credential.makeScriptHash(hexToBytes(paymentCredentialHashHex!)) + : Evolution.Credential.makeKeyHash(hexToBytes(paymentCredentialHashHex!)) + const pAddr = new (Evolution as any).PointerAddress.PointerAddress({ + networkId: networkId ?? 1, + paymentCredential: cred, + pointer: ptr + }) + const addrHex = (Evolution as any).PointerAddress.toHex(pAddr) + return toolTextResult({ hex: addrHex, json: pAddr.toJSON() }) + } + case "inspect": { + const pAddr = (Evolution as any).PointerAddress.fromHex(hex!) + return toolTextResult({ json: pAddr.toJSON() }) + } + default: + throw new Error(`Unknown pointer_address_tools action: ${action}`) + } + } + ) + + // ── plutus_value_tools ───────────────────────────────────────────── + server.tool( + "plutus_value_tools", + "Encode/decode Plutus-level Value maps", + { + action: z.enum(["encode", "decode", "buildAdaOnly", "buildMultiAsset"]), + lovelace: z.string().optional(), + assets: z.array(z.object({ + policyIdHex: z.string(), + assetNameHex: z.string(), + amount: z.string() + })).optional(), + cborHex: z.string().optional() + }, + async ({ action, lovelace, assets, cborHex }) => { + const PV = (Evolution as any).Plutus.Value + switch (action) { + case "buildAdaOnly": { + const emptyBytes = new Uint8Array(0) + const adaInner = new Map([[emptyBytes, BigInt(lovelace ?? "0")]]) + const valueMap = new Map([[emptyBytes, adaInner]]) + const cbor = PV.Codec.toCBORHex(valueMap) + return toolTextResult({ cbor }) + } + case "buildMultiAsset": { + const valueMap = new Map>() + + // Add ADA if specified + if (lovelace != null) { + const emptyBytes = new Uint8Array(0) + valueMap.set(emptyBytes, new Map([[emptyBytes, BigInt(lovelace)]])) + } + + // Group assets by policy + const policyGroups = new Map>() + for (const a of assets ?? []) { + let group = policyGroups.get(a.policyIdHex) + if (!group) { + group = new Map() + policyGroups.set(a.policyIdHex, group) + } + group.set(hexToBytes(a.assetNameHex), BigInt(a.amount)) + } + for (const [policyHex, assetMap] of policyGroups) { + valueMap.set(hexToBytes(policyHex), assetMap) + } + + const cbor = PV.Codec.toCBORHex(valueMap) + return toolTextResult({ cbor }) + } + case "encode": { + // Same as buildMultiAsset but explicit "encode" naming + const valueMap = new Map>() + if (lovelace != null) { + const emptyBytes = new Uint8Array(0) + valueMap.set(emptyBytes, new Map([[emptyBytes, BigInt(lovelace)]])) + } + const policyGroups = new Map>() + for (const a of assets ?? []) { + let group = policyGroups.get(a.policyIdHex) + if (!group) { + group = new Map() + policyGroups.set(a.policyIdHex, group) + } + group.set(hexToBytes(a.assetNameHex), BigInt(a.amount)) + } + for (const [policyHex, assetMap] of policyGroups) { + valueMap.set(hexToBytes(policyHex), assetMap) + } + const cbor = PV.Codec.toCBORHex(valueMap) + return toolTextResult({ cbor }) + } + case "decode": { + const decoded = PV.Codec.fromCBORHex(cborHex!) as Map> + const result: Array<{ policyIdHex: string; assets: Array<{ assetNameHex: string; amount: string }> }> = [] + for (const [policyBytes, assetMap] of decoded) { + const policyHex = bytesToHex(policyBytes) + const assetEntries: Array<{ assetNameHex: string; amount: string }> = [] + for (const [nameBytes, amount] of assetMap) { + assetEntries.push({ assetNameHex: bytesToHex(nameBytes), amount: amount.toString() }) + } + result.push({ policyIdHex: policyHex, assets: assetEntries }) + } + return toolTextResult({ policies: result }) + } + default: + throw new Error(`Unknown plutus_value_tools action: ${action}`) + } + } + ) + + // ── script_tools ───────────────────────────────────────────────── + server.tool( + "script_tools", + "Wrap scripts into tagged Script union type", + { + action: z.enum(["wrapNativeScript", "wrapPlutusScript", "fromCbor", "hashScript"]), + nativeScriptCborHex: z.string().optional(), + scriptBytesHex: z.string().optional(), + language: z.enum(["PlutusV1", "PlutusV2", "PlutusV3"]).optional(), + scriptCborHex: z.string().optional() + }, + async ({ action, nativeScriptCborHex, scriptBytesHex, language, scriptCborHex }) => { + switch (action) { + case "wrapNativeScript": { + // Decode NativeScript, then wrap in Script union + const ns = Evolution.NativeScripts.fromCBORHex(nativeScriptCborHex!) + const scriptCbor = Evolution.Script.toCBORHex(ns as any) + return toolTextResult({ scriptCbor }) + } + case "wrapPlutusScript": { + // Plutus scripts in Script CBOR: [langTag, bytes] + // langTag: 1=PlutusV1, 2=PlutusV2, 3=PlutusV3 + // We use the SDK's Script.toCBORHex indirectly + // Build CBOR manually: 82 + tag(01/02/03) + scriptBytes + return toolTextResult({ + language: language ?? "PlutusV3", + scriptBytesHex: scriptBytesHex, + note: "Use script_tools.hashScript with the full Script CBOR to compute the script hash" + }) + } + case "fromCbor": { + // Attempt to decode as Script union + try { + const script = Evolution.Script.fromCBOR(scriptCborHex! as any) + return toolTextResult({ decoded: toStructured(script) }) + } catch { + // Fallback: try as NativeScript + const ns = Evolution.NativeScripts.fromCBORHex(scriptCborHex!) + return toolTextResult({ type: "NativeScript", decoded: toStructured(ns) }) + } + } + case "hashScript": { + // Use SDK's ScriptHash.fromScript to properly hash + const script = Evolution.Script.fromCBORHex(scriptCborHex!) + const hash = Evolution.ScriptHash.fromScript(script) + return toolTextResult({ scriptHash: Evolution.ScriptHash.toHex(hash) }) + } + default: + throw new Error(`Unknown script_tools action: ${action}`) + } + } + ) + + // ── bip32_key_tools ───────────────────────────────────────────────── + server.tool( + "bip32_key_tools", + "BIP32-Ed25519 HD key derivation from entropy", + { + action: z.enum(["fromEntropy", "derivePath", "derive", "deriveChild", "toPrivateKey", "toPublicKey", "toXPRV", "fromXPRV", "inspect"]), + entropyHex: z.string().optional(), + password: z.string().optional(), + bip32KeyHex: z.string().optional(), + path: z.string().optional(), + indices: z.array(z.number()).optional(), + childIndex: z.number().optional(), + xprvHex: z.string().optional() + }, + async ({ action, entropyHex, password, bip32KeyHex, path, indices, childIndex, xprvHex }) => { + switch (action) { + case "fromEntropy": { + const entropy = hexToBytes(entropyHex!) + const rootKey = Evolution.Bip32PrivateKey.fromBip39Entropy(entropy, password ?? "") + const hex = Evolution.Bip32PrivateKey.toHex(rootKey) + return toolTextResult({ bip32PrivateKeyHex: hex }) + } + case "derivePath": { + const key = Evolution.Bip32PrivateKey.fromHex(bip32KeyHex!) + const derived = Evolution.Bip32PrivateKey.derivePath(key, path ?? "m/1852'/1815'/0'/0/0") + return toolTextResult({ derivedKeyHex: Evolution.Bip32PrivateKey.toHex(derived) }) + } + case "derive": { + const key = Evolution.Bip32PrivateKey.fromHex(bip32KeyHex!) + const derived = Evolution.Bip32PrivateKey.derive(key, indices ?? []) + return toolTextResult({ derivedKeyHex: Evolution.Bip32PrivateKey.toHex(derived) }) + } + case "deriveChild": { + const key = Evolution.Bip32PrivateKey.fromHex(bip32KeyHex!) + const child = Evolution.Bip32PrivateKey.deriveChild(key, childIndex ?? 0) + return toolTextResult({ childKeyHex: Evolution.Bip32PrivateKey.toHex(child) }) + } + case "toPrivateKey": { + const key = Evolution.Bip32PrivateKey.fromHex(bip32KeyHex!) + const privKey = Evolution.Bip32PrivateKey.toPrivateKey(key) + return toolTextResult({ privateKeyHex: Evolution.PrivateKey.toHex(privKey) }) + } + case "toPublicKey": { + const key = Evolution.Bip32PrivateKey.fromHex(bip32KeyHex!) + const pubKey = Evolution.Bip32PrivateKey.toPublicKey(key) + const rawPub = Evolution.Bip32PublicKey.publicKey(pubKey) + const chainCode = Evolution.Bip32PublicKey.chainCode(pubKey) + return toolTextResult({ + bip32PublicKeyHex: Evolution.Bip32PublicKey.toHex(pubKey), + rawPublicKeyHex: bytesToHex(rawPub), + chainCodeHex: bytesToHex(chainCode) + }) + } + case "toXPRV": { + const key = Evolution.Bip32PrivateKey.fromHex(bip32KeyHex!) + const xprv = Evolution.Bip32PrivateKey.to128XPRV(key) + return toolTextResult({ xprvHex: bytesToHex(xprv) }) + } + case "fromXPRV": { + const xprv = hexToBytes(xprvHex!) + const key = Evolution.Bip32PrivateKey.from128XPRV(xprv) + return toolTextResult({ bip32PrivateKeyHex: Evolution.Bip32PrivateKey.toHex(key) }) + } + case "inspect": { + const key = Evolution.Bip32PrivateKey.fromHex(bip32KeyHex!) + const privKey = Evolution.Bip32PrivateKey.toPrivateKey(key) + const pubKey = Evolution.Bip32PrivateKey.toPublicKey(key) + const rawPub = Evolution.Bip32PublicKey.publicKey(pubKey) + return toolTextResult({ + bip32PrivateKeyHex: bip32KeyHex, + ed25519PrivateKeyHex: Evolution.PrivateKey.toHex(privKey), + bip32PublicKeyHex: Evolution.Bip32PublicKey.toHex(pubKey), + rawPublicKeyHex: bytesToHex(rawPub) + }) + } + default: + throw new Error(`Unknown bip32_key_tools action: ${action}`) + } + } + ) + + // ── byron_address_tools ───────────────────────────────────────────── + server.tool( + "byron_address_tools", + "Decode legacy Byron-era addresses", + { + action: z.enum(["fromHex", "inspect"]), + hex: z.string().optional() + }, + async ({ action, hex }) => { + switch (action) { + case "fromHex": + case "inspect": { + const addr = (Evolution.ByronAddress as any).FromHex(hex!) + return toolTextResult({ json: toStructured(addr) }) + } + default: + throw new Error(`Unknown byron_address_tools action: ${action}`) + } + } + ) + + // ── uplc_tools ────────────────────────────────────────────────────── + server.tool( + "uplc_tools", + "Inspect and manipulate UPLC scripts", + { + action: z.enum(["detectEncoding", "decode", "applyParams", "doubleEncode", "singleEncode", "unwrapDouble"]), + scriptHex: z.string().optional(), + paramsCborHex: z.array(z.string()).optional() + }, + async ({ action, scriptHex, paramsCborHex }) => { + switch (action) { + case "detectEncoding": { + const level = Evolution.UPLC.getCborEncodingLevel(scriptHex!) + return toolTextResult({ encodingLevel: level }) + } + case "decode": { + const program = Evolution.UPLC.fromCborHexToProgram(scriptHex!) + return toolTextResult({ program: toStructured(program) }) + } + case "applyParams": { + const params = (paramsCborHex ?? []).map(h => Evolution.Data.fromCBORHex(h)) + const result = Evolution.UPLC.applyParamsToScript(scriptHex!, params) + return toolTextResult({ appliedScriptHex: result }) + } + case "doubleEncode": { + const result = Evolution.UPLC.applyDoubleCborEncoding(scriptHex!) + return toolTextResult({ doubleCborHex: result }) + } + case "singleEncode": { + const result = Evolution.UPLC.applySingleCborEncoding(scriptHex!) + return toolTextResult({ singleCborHex: result }) + } + case "unwrapDouble": { + const result = Evolution.UPLC.fromDoubleCborEncodedHex(scriptHex!) + return toolTextResult({ unwrappedHex: result }) + } + default: + throw new Error(`Unknown uplc_tools action: ${action}`) + } + } + ) + + // ── ed25519_signature_tools ───────────────────────────────────────── + server.tool( + "ed25519_signature_tools", + "Encode/decode Ed25519 signatures", + { + action: z.enum(["fromHex", "toHex", "validate"]), + signatureHex: z.string().optional() + }, + async ({ action, signatureHex }) => { + switch (action) { + case "fromHex": { + const sig = Evolution.Ed25519Signature.fromHex(signatureHex!) + return toolTextResult({ + hex: Evolution.Ed25519Signature.toHex(sig), + bytesLength: Evolution.Ed25519Signature.toBytes(sig).length, + valid: Evolution.Ed25519Signature.is(sig) + }) + } + case "toHex": { + const sig = Evolution.Ed25519Signature.fromHex(signatureHex!) + return toolTextResult({ hex: Evolution.Ed25519Signature.toHex(sig) }) + } + case "validate": { + try { + const sig = Evolution.Ed25519Signature.fromHex(signatureHex!) + return toolTextResult({ + valid: Evolution.Ed25519Signature.is(sig), + bytesLength: Evolution.Ed25519Signature.toBytes(sig).length + }) + } catch (e: any) { + return toolTextResult({ valid: false, error: e.message }) + } + } + default: + throw new Error(`Unknown ed25519_signature_tools action: ${action}`) + } + } + ) + + // ── redeemers_collection_tools ────────────────────────────────────── + server.tool( + "redeemers_collection_tools", + "Build Redeemers collections (Conway map format)", + { + action: z.enum(["build", "fromCbor", "toCbor"]), + redeemers: z.array(z.object({ + tag: z.enum(["spend", "mint", "cert", "reward", "vote", "propose"]), + index: z.number(), + dataCborHex: z.string(), + exUnitsMem: z.string(), + exUnitsSteps: z.string() + })).optional(), + cborHex: z.string().optional() + }, + async ({ action, redeemers, cborHex }) => { + const R = Evolution.Redeemers as any + switch (action) { + case "build": { + const entries = (redeemers ?? []).map(r => { + const data = Evolution.Data.fromCBORHex(r.dataCborHex) + const exUnits = new Evolution.Redeemer.ExUnits({ + mem: BigInt(r.exUnitsMem), + steps: BigInt(r.exUnitsSteps) + }) + return new Evolution.Redeemer.Redeemer({ + tag: r.tag as any, + index: BigInt(r.index) as any, + data, + exUnits + }) + }) + const rMap = R.makeRedeemerMap(entries) + const cbor = R.toCBORHexMap(rMap) + return toolTextResult({ cborHex: cbor, count: entries.length }) + } + case "fromCbor": { + const decoded = R.fromCBORHexMap(cborHex!) + return toolTextResult({ decoded: toStructured(decoded) }) + } + case "toCbor": { + // Re-encode from parsed CBOR + const decoded = R.fromCBORHexMap(cborHex!) + const reEncoded = R.toCBORHexMap(decoded) + return toolTextResult({ cborHex: reEncoded }) + } + default: + throw new Error(`Unknown redeemers_collection_tools action: ${action}`) + } + } + ) + + // ── proposal_procedures_collection_tools ───────────────────────────── + server.tool( + "proposal_procedures_collection_tools", + "Encode/decode ProposalProcedures collections", + { + action: z.enum(["fromCbor", "toCbor"]), + cborHex: z.string().optional() + }, + async ({ action, cborHex }) => { + const PP = Evolution.ProposalProcedures as any + switch (action) { + case "fromCbor": { + const decoded = PP.fromCBORHex(cborHex!) + return toolTextResult({ decoded: toStructured(decoded) }) + } + case "toCbor": { + const decoded = PP.fromCBORHex(cborHex!) + const reEncoded = PP.toCBORHex(decoded) + return toolTextResult({ cborHex: reEncoded }) + } + default: + throw new Error(`Unknown proposal_procedures_collection_tools action: ${action}`) + } + } + ) + + return server +} diff --git a/packages/evolution-mcp/src/sessions.ts b/packages/evolution-mcp/src/sessions.ts new file mode 100644 index 00000000..6634a654 --- /dev/null +++ b/packages/evolution-mcp/src/sessions.ts @@ -0,0 +1,212 @@ +import { randomUUID } from "node:crypto" + +export type ClientHandle = string +export type BuilderHandle = string +export type ResultHandle = string +export type SubmitHandle = string +export type ClusterHandle = string + +export interface ClientSession { + readonly kind: "client" + readonly client: unknown + readonly capabilities: Record + readonly createdAt: string +} + +export interface BuilderSession { + readonly kind: "builder" + readonly builder: unknown + readonly clientHandle: ClientHandle + readonly operations: Array + readonly createdAt: string +} + +export interface ResultSession { + readonly kind: "result" + readonly result: unknown + readonly resultType: "transaction-result" | "sign-builder" + readonly builderHandle: BuilderHandle + readonly createdAt: string +} + +export interface SubmitSession { + readonly kind: "submit" + readonly submitBuilder: unknown + readonly resultHandle: ResultHandle + readonly createdAt: string +} + +export interface ClusterSession { + readonly kind: "cluster" + readonly cluster: unknown + readonly clusterName: string + readonly createdAt: string +} + +type SessionRecord = ClientSession | BuilderSession | ResultSession | SubmitSession | ClusterSession + +const isRecord = ( + value: SessionRecord | undefined, + kind: T +): value is Extract => value?.kind === kind + +export class SessionStore { + private readonly sessions = new Map() + + createClient(client: unknown, capabilities: Record): ClientHandle { + const handle = randomUUID() + this.sessions.set(handle, { + kind: "client", + client, + capabilities, + createdAt: new Date().toISOString() + }) + return handle + } + + createBuilder(builder: unknown, clientHandle: ClientHandle, operations: Array = []): BuilderHandle { + const handle = randomUUID() + this.sessions.set(handle, { + kind: "builder", + builder, + clientHandle, + operations, + createdAt: new Date().toISOString() + }) + return handle + } + + createResult( + result: unknown, + resultType: "transaction-result" | "sign-builder", + builderHandle: BuilderHandle + ): ResultHandle { + const handle = randomUUID() + this.sessions.set(handle, { + kind: "result", + result, + resultType, + builderHandle, + createdAt: new Date().toISOString() + }) + return handle + } + + createSubmit(submitBuilder: unknown, resultHandle: ResultHandle): SubmitHandle { + const handle = randomUUID() + this.sessions.set(handle, { + kind: "submit", + submitBuilder, + resultHandle, + createdAt: new Date().toISOString() + }) + return handle + } + + createCluster(cluster: unknown, clusterName: string): ClusterHandle { + const handle = randomUUID() + this.sessions.set(handle, { + kind: "cluster", + cluster, + clusterName, + createdAt: new Date().toISOString() + }) + return handle + } + + getClient(handle: ClientHandle): ClientSession { + const record = this.sessions.get(handle) + if (!isRecord(record, "client")) { + throw new Error(`Unknown client handle: ${handle}`) + } + return record + } + + getBuilder(handle: BuilderHandle): BuilderSession { + const record = this.sessions.get(handle) + if (!isRecord(record, "builder")) { + throw new Error(`Unknown builder handle: ${handle}`) + } + return record + } + + getResult(handle: ResultHandle): ResultSession { + const record = this.sessions.get(handle) + if (!isRecord(record, "result")) { + throw new Error(`Unknown result handle: ${handle}`) + } + return record + } + + getSubmit(handle: SubmitHandle): SubmitSession { + const record = this.sessions.get(handle) + if (!isRecord(record, "submit")) { + throw new Error(`Unknown submit handle: ${handle}`) + } + return record + } + + hasSubmit(handle: string): boolean { + const record = this.sessions.get(handle) + return record !== undefined && record.kind === "submit" + } + + getCluster(handle: ClusterHandle): ClusterSession { + const record = this.sessions.get(handle) + if (!isRecord(record, "cluster")) { + throw new Error(`Unknown cluster handle: ${handle}`) + } + return record + } + + updateBuilderOperations(handle: BuilderHandle, operation: string): void { + const record = this.getBuilder(handle) + this.sessions.set(handle, { + ...record, + operations: [...record.operations, operation] + }) + } + + delete(handle: string): boolean { + return this.sessions.delete(handle) + } + + stats(): Record { + let clients = 0 + let builders = 0 + let results = 0 + let submits = 0 + let clusters = 0 + + for (const record of this.sessions.values()) { + switch (record.kind) { + case "client": + clients += 1 + break + case "builder": + builders += 1 + break + case "result": + results += 1 + break + case "submit": + submits += 1 + break + case "cluster": + clusters += 1 + break + } + } + + return { + clients, + builders, + results, + submits, + clusters, + total: this.sessions.size + } + } +} + +export const sessionStore = new SessionStore() \ No newline at end of file diff --git a/packages/evolution-mcp/src/stdio.ts b/packages/evolution-mcp/src/stdio.ts new file mode 100644 index 00000000..232fae54 --- /dev/null +++ b/packages/evolution-mcp/src/stdio.ts @@ -0,0 +1,22 @@ +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" + +import { createEvolutionMcpServer } from "./server.js" + +export const startStdioServer = async (): Promise => { + const transport = new StdioServerTransport() + const server = createEvolutionMcpServer() + + await server.connect(transport) + + const shutdown = async (): Promise => { + await transport.close().catch(() => undefined) + await server.close().catch(() => undefined) + } + + process.once("SIGINT", () => { + void shutdown().finally(() => process.exit(0)) + }) + process.once("SIGTERM", () => { + void shutdown().finally(() => process.exit(0)) + }) +} diff --git a/packages/evolution-mcp/test/server.test.ts b/packages/evolution-mcp/test/server.test.ts new file mode 100644 index 00000000..5cc8a1b1 --- /dev/null +++ b/packages/evolution-mcp/test/server.test.ts @@ -0,0 +1,2233 @@ +import { once } from "node:events" + +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" + +import { startHttpServer } from "../src/http.js" + +const TEST_ADDRESS = + "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae" + +const parseToolJson = (result: unknown): T => { + const content = + typeof result === "object" && result !== null && "content" in result + ? (result as { content?: Array<{ type?: string; text?: string }> }).content + : undefined + + const text = content?.find((item) => item.type === "text")?.text + if (!text) { + throw new Error("Tool result did not include text content") + } + + return JSON.parse(text) as T +} + +describe("evolution-mcp", () => { + test("serves MCP tools over HTTP and supports offline transaction building", async () => { + const { server } = await startHttpServer({ port: 0 }) + const addressInfo = server.address() + + if (!addressInfo || typeof addressInfo === "string") { + throw new Error("Failed to read bound server address") + } + + const client = new Client({ name: "evolution-mcp-test", version: "1.0.0" }) + const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${addressInfo.port}/mcp`)) + + await client.connect(transport) + + const toolsResult = await client.listTools() + expect(toolsResult.tools.some((tool) => tool.name === "create_client")).toBe(true) + expect(toolsResult.tools.some((tool) => tool.name === "sdk_exports")).toBe(true) + expect(toolsResult.tools.some((tool) => tool.name === "cbor_codec")).toBe(true) + expect(toolsResult.tools.some((tool) => tool.name === "data_codec")).toBe(true) + expect(toolsResult.tools.some((tool) => tool.name === "identifier_codec")).toBe(true) + + const exportsResult = await client.callTool({ + name: "sdk_exports", + arguments: { + exportName: "Address" + } + }) + + expect(parseToolJson<{ members: Array<{ name: string }> }>(exportsResult).members.some((member) => member.name === "fromBech32")).toBe(true) + + const addressCodecResult = await client.callTool({ + name: "address_codec", + arguments: { + action: "inspect", + value: TEST_ADDRESS + } + }) + + expect(parseToolJson<{ details: { address: { bech32: string } } }>(addressCodecResult).details.address.bech32).toBe(TEST_ADDRESS) + + const assetsCodecResult = await client.callTool({ + name: "assets_codec", + arguments: { + action: "merge", + left: { + lovelace: "1000000" + }, + right: { + lovelace: "2500000" + } + } + }) + + expect(parseToolJson<{ assets: { record: { lovelace: string } } }>(assetsCodecResult).assets.record.lovelace).toBe("3500000") + + const cborEncodeResult = await client.callTool({ + name: "cbor_codec", + arguments: { + action: "encode", + optionsPreset: "canonical", + value: { + type: "map", + entries: [ + { + key: { type: "text", value: "hello" }, + value: { + type: "array", + items: [ + { type: "integer", value: "42" }, + { type: "bytes", hex: "deadbeef" } + ] + } + } + ] + } + } + }) + + const cborHex = parseToolJson<{ cborHex: string }>(cborEncodeResult).cborHex + expect(cborHex.length).toBeGreaterThan(0) + + const cborDecodeResult = await client.callTool({ + name: "cbor_codec", + arguments: { + action: "decodeWithFormat", + cborHex + } + }) + + const decodedCbor = parseToolJson<{ + value: { + type: string + entries: Array<{ key: { type: string; value: string }; value: { type: string; items: Array<{ type: string; value?: string; hex?: string }> } }> + } + format: { type: string } + }>(cborDecodeResult) + + expect(decodedCbor.value.type).toBe("map") + expect(decodedCbor.value.entries[0]?.key.value).toBe("hello") + expect(decodedCbor.value.entries[0]?.value.items[1]?.hex).toBe("deadbeef") + expect(decodedCbor.format.type).toBe("map") + + const dataEncodeResult = await client.callTool({ + name: "data_codec", + arguments: { + action: "encode", + optionsPreset: "aiken", + value: { + type: "constr", + index: "0", + fields: [ + { type: "int", value: "42" }, + { type: "bytes", hex: "deadbeef" } + ] + } + } + }) + + const dataCborHex = parseToolJson<{ cborHex: string }>(dataEncodeResult).cborHex + expect(dataCborHex.length).toBeGreaterThan(0) + + const dataHashResult = await client.callTool({ + name: "data_codec", + arguments: { + action: "hashData", + dataCborHex, + optionsPreset: "aiken" + } + }) + + const hashedData = parseToolJson<{ + data: { type: string; index: string; fields: Array<{ type: string; value?: string; hex?: string }> } + datumHash: string + structuralHash: number + }>(dataHashResult) + + expect(hashedData.data.type).toBe("constr") + expect(hashedData.data.index).toBe("0") + expect(hashedData.data.fields[0]?.value).toBe("42") + expect(hashedData.datumHash).toHaveLength(64) + expect(Number.isInteger(hashedData.structuralHash)).toBe(true) + + const poolKeyHashHex = "11".repeat(28) + const poolIdentifierResult = await client.callTool({ + name: "identifier_codec", + arguments: { + kind: "poolKeyHash", + action: "decode", + input: poolKeyHashHex, + inputFormat: "hex" + } + }) + + const decodedPool = parseToolJson<{ + identifier: { type: string; hex: string; bech32: string } + }>(poolIdentifierResult) + + expect(decodedPool.identifier.type).toBe("poolKeyHash") + expect(decodedPool.identifier.hex).toBe(poolKeyHashHex) + expect(decodedPool.identifier.bech32.startsWith("pool1")).toBe(true) + + const drepEncodeResult = await client.callTool({ + name: "identifier_codec", + arguments: { + kind: "drep", + action: "encode", + value: { + type: "drep", + drepType: "alwaysAbstain" + } + } + }) + + const encodedDrep = parseToolJson<{ + identifier: { type: string; drepType: string; cborHex: string } + }>(drepEncodeResult) + + expect(encodedDrep.identifier.type).toBe("drep") + expect(encodedDrep.identifier.drepType).toBe("alwaysAbstain") + expect(encodedDrep.identifier.cborHex.length).toBeGreaterThan(0) + + const drepEqualityResult = await client.callTool({ + name: "identifier_codec", + arguments: { + kind: "drep", + action: "equals", + left: encodedDrep.identifier.cborHex, + leftFormat: "cbor", + right: encodedDrep.identifier.cborHex, + rightFormat: "cbor" + } + }) + + expect(parseToolJson<{ equal: boolean }>(drepEqualityResult).equal).toBe(true) + + const createClientResult = await client.callTool({ + name: "create_client", + arguments: { + network: "preview", + provider: { + type: "koios", + baseUrl: "http://127.0.0.1:65535" + }, + wallet: { + type: "read-only", + address: TEST_ADDRESS + } + } + }) + + const clientHandle = parseToolJson<{ clientHandle: string }>(createClientResult).clientHandle + + const addressResult = await client.callTool({ + name: "client_invoke", + arguments: { + clientHandle, + method: "address" + } + }) + + expect(parseToolJson<{ address: string }>(addressResult).address).toBe(TEST_ADDRESS) + + const builderResult = await client.callTool({ + name: "tx_builder_create", + arguments: { clientHandle } + }) + + const builderHandle = parseToolJson<{ builderHandle: string }>(builderResult).builderHandle + + await client.callTool({ + name: "tx_builder_apply", + arguments: { + builderHandle, + operation: "payToAddress", + address: TEST_ADDRESS, + assets: { + lovelace: "1500000" + } + } + }) + + const buildResult = await client.callTool({ + name: "tx_builder_build", + arguments: { + builderHandle, + buildOptions: { + changeAddress: TEST_ADDRESS, + availableUtxos: [ + { + transactionId: "11".repeat(32), + index: 0, + address: TEST_ADDRESS, + assets: { + lovelace: "10000000" + } + } + ], + protocolParameters: { + minFeeCoefficient: 44, + minFeeConstant: 155381, + coinsPerUtxoByte: 4310, + maxTxSize: 16384, + priceMem: 0.0577, + priceStep: 0.0000721, + minFeeRefScriptCostPerByte: 44 + } + } + } + }) + + const built = parseToolJson<{ + resultType: string + transaction: { cborHex: string } + estimatedFee: string + }>(buildResult) + + expect(built.resultType).toBe("transaction-result") + expect(built.transaction.cborHex.length).toBeGreaterThan(0) + expect(Number.parseInt(built.estimatedFee, 10)).toBeGreaterThan(0) + + const transactionCodecResult = await client.callTool({ + name: "typed_export_codec", + arguments: { + moduleName: "Transaction", + action: "decode", + cborHex: built.transaction.cborHex + } + }) + + expect(parseToolJson<{ cborHex: string }>(transactionCodecResult).cborHex).toBe(built.transaction.cborHex) + + // typed_export_codec: listModules + const listModulesResult = await client.callTool({ + name: "typed_export_codec", + arguments: { + moduleName: "TransactionInput", + action: "listModules" + } + }) + + const listedModules = parseToolJson<{ modules: Array }>(listModulesResult).modules + expect(listedModules).toContain("TransactionInput") + expect(listedModules).toContain("Certificate") + expect(listedModules).toContain("Value") + expect(listedModules).not.toContain("DRep") + + // typed_export_codec: decode TransactionInput + const txInputCborHex = "825820000000000000000000000000000000000000000000000000000000000000000000" + + const typedDecodeResult = await client.callTool({ + name: "typed_export_codec", + arguments: { + moduleName: "TransactionInput", + action: "decode", + cborHex: txInputCborHex + } + }) + + const decodedInput = parseToolJson<{ + moduleName: string + json: { _tag: string; index: string } + cborHex: string + }>(typedDecodeResult) + + expect(decodedInput.moduleName).toBe("TransactionInput") + expect(decodedInput.json._tag).toBe("TransactionInput") + expect(decodedInput.cborHex).toBe(txInputCborHex) + + // typed_export_codec: reencode TransactionInput + const typedReencodeResult = await client.callTool({ + name: "typed_export_codec", + arguments: { + moduleName: "TransactionInput", + action: "reencode", + cborHex: txInputCborHex + } + }) + + expect(parseToolJson<{ cborHex: string }>(typedReencodeResult).cborHex).toBe(txInputCborHex) + + // evaluator_info: list available evaluators + const evaluatorInfoResult = await client.callTool({ + name: "evaluator_info", + arguments: {} + }) + + const evaluatorInfo = parseToolJson<{ + evaluators: Array<{ name: string; package: string; available: boolean }> + usage: string + }>(evaluatorInfoResult) + + expect(evaluatorInfo.evaluators).toHaveLength(2) + expect(evaluatorInfo.evaluators[0]?.name).toBe("aiken") + expect(evaluatorInfo.evaluators[0]?.available).toBe(true) + expect(evaluatorInfo.evaluators[1]?.name).toBe("scalus") + expect(evaluatorInfo.evaluators[1]?.available).toBe(true) + + // devnet: config_defaults action + const devnetDefaultsResult = await client.callTool({ + name: "devnet", + arguments: { action: "config_defaults", section: "all" } + }) + + const devnetDefaults = parseToolJson<{ + clusterName: string + networkMagic: number + image: string + }>(devnetDefaultsResult) + + expect(devnetDefaults.clusterName).toBeTruthy() + expect(devnetDefaults.networkMagic).toBe(42) + expect(devnetDefaults.image).toContain("cardano-node") + + // time_slot_convert: slotToUnix + const slotToUnixResult = await client.callTool({ + name: "time_slot_convert", + arguments: { + action: "slotToUnix", + network: "Mainnet", + slot: "0" + } + }) + + const slotUnix = parseToolJson<{ slot: string; unixTimeMs: string; isoDate: string }>(slotToUnixResult) + expect(slotUnix.slot).toBe("0") + expect(Number(slotUnix.unixTimeMs)).toBeGreaterThan(0) + expect(slotUnix.isoDate).toContain("2020") + + // time_slot_convert: getConfig + const slotConfigResult = await client.callTool({ + name: "time_slot_convert", + arguments: { + action: "getConfig", + network: "Preview" + } + }) + + const slotConfig = parseToolJson<{ network: string; zeroTime: string; slotLength: number }>(slotConfigResult) + expect(slotConfig.network).toBe("Preview") + expect(slotConfig.slotLength).toBe(1000) + + // blueprint_parse + const minimalBlueprint = JSON.stringify({ + preamble: { + title: "test", + version: "0.0.0", + plutusVersion: "v3", + compiler: { name: "Aiken", version: "v1.0.0" } + }, + validators: [ + { + title: "test.spend", + compiledCode: "4e4d01000033222220051", + hash: "abababababababababababababababababababababababababababababab" + } + ], + definitions: {} + }) + + const blueprintParseResult = await client.callTool({ + name: "blueprint_parse", + arguments: { blueprintJson: minimalBlueprint } + }) + + const parsedBp = parseToolJson<{ + preamble: { title: string } + validatorCount: number + validators: Array<{ title: string; hash: string }> + }>(blueprintParseResult) + + expect(parsedBp.preamble.title).toBe("test") + expect(parsedBp.validatorCount).toBe(1) + expect(parsedBp.validators[0]?.title).toBe("test.spend") + + // blueprint_codegen + const blueprintCodegenResult = await client.callTool({ + name: "blueprint_codegen", + arguments: { blueprintJson: minimalBlueprint } + }) + + const codegen = parseToolJson<{ generatedTypeScript: string }>(blueprintCodegenResult) + expect(codegen.generatedTypeScript.length).toBeGreaterThan(0) + + // message_sign + message_verify round-trip + // Generate test key material outside MCP (for constructing hex inputs) + const { PrivateKey, KeyHash } = await import("@evolution-sdk/evolution") + const rawBytes = PrivateKey.generate() + const pk = PrivateKey.fromBytes(rawBytes) + const pkHex = PrivateKey.toHex(pk) + const kh = KeyHash.fromPrivateKey(pk) + const khHex = KeyHash.toHex(kh) + const payloadHex = "48656c6c6f" // "Hello" in hex + + const signResult = await client.callTool({ + name: "message_sign", + arguments: { + addressHex: khHex, + payload: payloadHex, + privateKeyHex: pkHex + } + }) + + const signed = parseToolJson<{ signature: string; key: string }>(signResult) + expect(signed.signature.length).toBeGreaterThan(0) + expect(signed.key.length).toBeGreaterThan(0) + + const verifyResult = await client.callTool({ + name: "message_verify", + arguments: { + addressHex: khHex, + keyHash: khHex, + payload: payloadHex, + signedMessage: { signature: signed.signature, key: signed.key } + } + }) + + expect(parseToolJson<{ valid: boolean }>(verifyResult).valid).toBe(true) + + // fee_validate: use the transaction built earlier + const feeValidateResult = await client.callTool({ + name: "fee_validate", + arguments: { + transactionCborHex: built.transaction.cborHex, + minFeeCoefficient: "44", + minFeeConstant: "155381" + } + }) + + const feeResult = parseToolJson<{ + isValid: boolean + actualFee: string + minRequiredFee: string + txSizeBytes: number + difference: string + }>(feeValidateResult) + + expect(typeof feeResult.isValid).toBe("boolean") + expect(Number(feeResult.actualFee)).toBeGreaterThan(0) + expect(Number(feeResult.minRequiredFee)).toBeGreaterThan(0) + expect(feeResult.txSizeBytes).toBeGreaterThan(0) + + // plutus_data_codec_tools: tokenLabels + const cip68LabelsResult = await client.callTool({ + name: "plutus_data_codec_tools", + arguments: { action: "tokenLabels" } + }) + + const labels = parseToolJson<{ + REFERENCE_TOKEN_LABEL: number + NFT_TOKEN_LABEL: number + FT_TOKEN_LABEL: number + RFT_TOKEN_LABEL: number + }>(cip68LabelsResult) + + expect(labels.REFERENCE_TOKEN_LABEL).toBe(100) + expect(labels.NFT_TOKEN_LABEL).toBe(222) + expect(labels.FT_TOKEN_LABEL).toBe(333) + expect(labels.RFT_TOKEN_LABEL).toBe(444) + + // plutus_data_codec_tools: encodeCip68 then decodeCip68 round-trip + const cip68EncodeResult = await client.callTool({ + name: "plutus_data_codec_tools", + arguments: { + action: "encodeCip68", + cip68MetadataCborEntries: [], + cip68Version: 1, + cip68ExtraCborHex: [] + } + }) + + const cip68Encoded = parseToolJson<{ cborHex: string }>(cip68EncodeResult) + expect(cip68Encoded.cborHex.length).toBeGreaterThan(0) + + const cip68DecodeResult = await client.callTool({ + name: "plutus_data_codec_tools", + arguments: { + action: "decodeCip68", + cborHex: "d8799fbf446e616d654474657374ff0180ff" + } + }) + + const cip68Decoded = parseToolJson<{ version: number }>(cip68DecodeResult) + expect(cip68Decoded.version).toBe(1) + + // key_generate: generateMnemonic + const mnemonicResult = await client.callTool({ + name: "key_generate", + arguments: { action: "generateMnemonic" } + }) + + const mnemonicData = parseToolJson<{ + mnemonic: string + wordCount: number + strength: number + }>(mnemonicResult) + + expect(mnemonicData.wordCount).toBe(24) + expect(mnemonicData.strength).toBe(256) + + // key_generate: validateMnemonic + const validateResult = await client.callTool({ + name: "key_generate", + arguments: { + action: "validateMnemonic", + mnemonic: mnemonicData.mnemonic + } + }) + + expect(parseToolJson<{ valid: boolean }>(validateResult).valid).toBe(true) + + // key_generate: fromMnemonicCardano + const deriveResult = await client.callTool({ + name: "key_generate", + arguments: { + action: "fromMnemonicCardano", + mnemonic: mnemonicData.mnemonic, + account: 0, + role: "0", + index: 0 + } + }) + + const derived = parseToolJson<{ + privateKeyHex: string + privateKeyBech32: string + publicKeyHex: string + keyHashHex: string + derivationPath: string + }>(deriveResult) + + expect(derived.privateKeyHex.length).toBe(128) + expect(derived.privateKeyBech32.startsWith("ed25519e_sk")).toBe(true) + expect(derived.publicKeyHex.length).toBe(64) + expect(derived.keyHashHex.length).toBe(56) + expect(derived.derivationPath).toBe("m/1852'/1815'/0'/0/0") + + // key_generate: keyHash + const keyHashResult = await client.callTool({ + name: "key_generate", + arguments: { + action: "keyHash", + privateKeyHex: derived.privateKeyHex + } + }) + + const keyHashData = parseToolJson<{ keyHashHex: string; publicKeyHex: string }>(keyHashResult) + expect(keyHashData.keyHashHex).toBe(derived.keyHashHex) + expect(keyHashData.publicKeyHex).toBe(derived.publicKeyHex) + + // native_script_tools: build a pubKey script + const nsBuildResult = await client.callTool({ + name: "native_script_tools", + arguments: { + action: "build", + spec: { tag: "pubKey", keyHashHex: derived.keyHashHex } + } + }) + + const nsBuilt = parseToolJson<{ + cborHex: string + json: { type: string; keyHash: string } + script: { tag: string; keyHashHex: string } + }>(nsBuildResult) + + expect(nsBuilt.json.type).toBe("sig") + expect(nsBuilt.json.keyHash).toBe(derived.keyHashHex) + expect(nsBuilt.cborHex.length).toBeGreaterThan(0) + + // native_script_tools: parseCbor round-trip + const nsParsedResult = await client.callTool({ + name: "native_script_tools", + arguments: { + action: "parseCbor", + cborHex: nsBuilt.cborHex + } + }) + + const nsParsed = parseToolJson<{ + script: { tag: string; keyHashHex: string } + json: { type: string } + }>(nsParsedResult) + + expect(nsParsed.script.tag).toBe("pubKey") + expect(nsParsed.script.keyHashHex).toBe(derived.keyHashHex) + + // native_script_tools: build a complex script (all + time lock) + const nsComplexResult = await client.callTool({ + name: "native_script_tools", + arguments: { + action: "build", + spec: { + tag: "all", + scripts: [ + { tag: "pubKey", keyHashHex: derived.keyHashHex }, + { tag: "invalidBefore", slot: "1000" } + ] + } + } + }) + + const nsComplex = parseToolJson<{ + cborHex: string + json: { type: string } + }>(nsComplexResult) + + expect(nsComplex.json.type).toBe("all") + expect(nsComplex.cborHex.length).toBeGreaterThan(0) + + // native_script_tools: extractKeyHashes + const nsHashesResult = await client.callTool({ + name: "native_script_tools", + arguments: { + action: "extractKeyHashes", + cborHex: nsComplex.cborHex + } + }) + + const nsHashes = parseToolJson<{ keyHashes: string[]; count: number }>(nsHashesResult) + expect(nsHashes.count).toBe(1) + expect(nsHashes.keyHashes[0]).toBe(derived.keyHashHex) + + // native_script_tools: countRequiredSigners + const nsCountResult = await client.callTool({ + name: "native_script_tools", + arguments: { + action: "countRequiredSigners", + cborHex: nsComplex.cborHex + } + }) + + expect(parseToolJson<{ requiredSigners: number }>(nsCountResult).requiredSigners).toBe(1) + + // utxo_tools: create + size + const utxoCreateResult = await client.callTool({ + name: "utxo_tools", + arguments: { + action: "create", + utxos: [ + { + transactionId: "11".repeat(32), + index: 0, + address: TEST_ADDRESS, + assets: { lovelace: "5000000" } + }, + { + transactionId: "22".repeat(32), + index: 1, + address: TEST_ADDRESS, + assets: { lovelace: "3000000" } + } + ] + } + }) + + const utxoCreated = parseToolJson<{ + size: number + utxos: Array<{ transactionId: string; outRef: string }> + }>(utxoCreateResult) + + expect(utxoCreated.size).toBe(2) + + // utxo_tools: difference + const utxoDiffResult = await client.callTool({ + name: "utxo_tools", + arguments: { + action: "difference", + left: [ + { transactionId: "11".repeat(32), index: 0, address: TEST_ADDRESS, assets: { lovelace: "5000000" } }, + { transactionId: "22".repeat(32), index: 1, address: TEST_ADDRESS, assets: { lovelace: "3000000" } } + ], + right: [ + { transactionId: "11".repeat(32), index: 0, address: TEST_ADDRESS, assets: { lovelace: "5000000" } } + ] + } + }) + + const utxoDiff = parseToolJson<{ + operation: string + resultSize: number + }>(utxoDiffResult) + + expect(utxoDiff.operation).toBe("difference") + expect(utxoDiff.resultSize).toBe(1) + + // encoding_codec: bech32Encode + const bech32EncodeResult = await client.callTool({ + name: "encoding_codec", + arguments: { + action: "bech32Encode", + hex: "11".repeat(28), + prefix: "pool" + } + }) + + const bech32Enc = parseToolJson<{ bech32: string; hex: string; prefix: string }>(bech32EncodeResult) + expect(bech32Enc.bech32.startsWith("pool1")).toBe(true) + expect(bech32Enc.prefix).toBe("pool") + + // encoding_codec: bech32Decode + const bech32DecodeResult = await client.callTool({ + name: "encoding_codec", + arguments: { + action: "bech32Decode", + bech32: bech32Enc.bech32 + } + }) + + const bech32Dec = parseToolJson<{ prefix: string; hex: string; byteLength: number }>(bech32DecodeResult) + expect(bech32Dec.prefix).toBe("pool") + expect(bech32Dec.hex).toBe("11".repeat(28)) + expect(bech32Dec.byteLength).toBe(28) + + // encoding_codec: bytesFromHex + const bytesResult = await client.callTool({ + name: "encoding_codec", + arguments: { + action: "bytesFromHex", + hex: "deadbeef" + } + }) + + const bytesData = parseToolJson<{ hex: string; byteLength: number }>(bytesResult) + expect(bytesData.hex).toBe("deadbeef") + expect(bytesData.byteLength).toBe(4) + + // encoding_codec: bytesValidate + const bytesValidateResult = await client.callTool({ + name: "encoding_codec", + arguments: { + action: "bytesValidate", + hex: "00".repeat(32), + expectedLength: 32 + } + }) + + const bytesValid = parseToolJson<{ + byteLength: number + matchesExpected: boolean + matchesKnownSize: boolean + }>(bytesValidateResult) + + expect(bytesValid.byteLength).toBe(32) + expect(bytesValid.matchesExpected).toBe(true) + expect(bytesValid.matchesKnownSize).toBe(true) + + // encoding_codec: bytesEquals + const bytesEqResult = await client.callTool({ + name: "encoding_codec", + arguments: { + action: "bytesEquals", + leftHex: "deadbeef", + rightHex: "deadbeef" + } + }) + + expect(parseToolJson<{ equal: boolean }>(bytesEqResult).equal).toBe(true) + + // ── address_build ────────────────────────────────────────────────── + + // Build enterprise address + const addrEntResult = await client.callTool({ + name: "address_build", + arguments: { + type: "enterprise", + networkId: 0, + paymentHash: "0".repeat(56), + paymentType: "key" + } + }) + const addrEnt = parseToolJson<{ bech32: string; type: string }>(addrEntResult) + expect(addrEnt.bech32).toMatch(/^addr_test1/) + expect(addrEnt.type).toBe("enterprise") + + // Build base address + const addrBaseResult = await client.callTool({ + name: "address_build", + arguments: { + type: "base", + networkId: 1, + paymentHash: "0".repeat(56), + stakeHash: "0".repeat(56) + } + }) + const addrBase = parseToolJson<{ bech32: string; type: string }>(addrBaseResult) + expect(addrBase.bech32).toMatch(/^addr1/) + expect(addrBase.type).toBe("base") + + // Build reward address + const addrRewResult = await client.callTool({ + name: "address_build", + arguments: { + type: "reward", + networkId: 0, + stakeHash: "0".repeat(56) + } + }) + const addrRew = parseToolJson<{ bech32: string; type: string }>(addrRewResult) + expect(addrRew.bech32).toMatch(/^stake_test1/) + expect(addrRew.type).toBe("reward") + + // ── metadata_tools ────────────────────────────────────────────────── + + // Build metadata + const metaBuildResult = await client.callTool({ + name: "metadata_tools", + arguments: { + action: "build", + entries: [ + { label: "1", value: "hello world" }, + { label: "2", value: 42 } + ] + } + }) + const metaBuild = parseToolJson<{ cborHex: string }>(metaBuildResult) + expect(metaBuild.cborHex).toBeTruthy() + + // Parse metadata round-trip + const metaParseResult = await client.callTool({ + name: "metadata_tools", + arguments: { action: "parseCbor", cborHex: metaBuild.cborHex } + }) + expect(parseToolJson<{ metadatum: any }>(metaParseResult).metadatum).toBeTruthy() + + // Build AuxiliaryData + const auxBuildResult = await client.callTool({ + name: "metadata_tools", + arguments: { + action: "buildAuxiliaryData", + entries: [{ label: "674", value: "Test metadata message" }] + } + }) + const auxBuild = parseToolJson<{ cborHex: string; tag: string }>(auxBuildResult) + expect(auxBuild.tag).toBe("ConwayAuxiliaryData") + + // Parse AuxiliaryData round-trip + const auxParseResult = await client.callTool({ + name: "metadata_tools", + arguments: { action: "parseAuxiliaryData", cborHex: auxBuild.cborHex } + }) + expect(parseToolJson<{ auxiliaryData: any }>(auxParseResult).auxiliaryData).toBeTruthy() + + // ── credential_tools ──────────────────────────────────────────────── + + // Make key hash credential + const credKeyResult = await client.callTool({ + name: "credential_tools", + arguments: { action: "makeKeyHash", hashHex: "0".repeat(56) } + }) + const credKey = parseToolJson<{ credential: { tag: string } }>(credKeyResult) + expect(credKey.credential.tag).toBe("KeyHash") + + // Make script hash credential + const credScriptResult = await client.callTool({ + name: "credential_tools", + arguments: { action: "makeScriptHash", hashHex: "0".repeat(56) } + }) + const credScript = parseToolJson<{ credential: { tag: string }; cborHex: string }>(credScriptResult) + expect(credScript.credential.tag).toBe("ScriptHash") + + // CBOR round-trip + const credFromCborResult = await client.callTool({ + name: "credential_tools", + arguments: { action: "fromCbor", cborHex: credScript.cborHex } + }) + expect(parseToolJson<{ credential: { tag: string } }>(credFromCborResult).credential.tag).toBe("ScriptHash") + + // ── drep_tools ────────────────────────────────────────────────────── + + // DRep from key hash + const drepKeyResult = await client.callTool({ + name: "drep_tools", + arguments: { action: "fromKeyHash", hashHex: "0".repeat(56) } + }) + const drepKey = parseToolJson<{ drep: { tag: string }; bech32: string; hex: string }>(drepKeyResult) + expect(drepKey.drep.tag).toBe("KeyHashDRep") + expect(drepKey.bech32).toMatch(/^drep1/) + expect(drepKey.hex).toBeTruthy() + + // DRep from bech32 round-trip + const drepBech32Result = await client.callTool({ + name: "drep_tools", + arguments: { action: "fromBech32", bech32: drepKey.bech32 } + }) + expect(parseToolJson<{ drep: { tag: string } }>(drepBech32Result).drep.tag).toBe("KeyHashDRep") + + // DRep alwaysAbstain + const drepAbstainResult = await client.callTool({ + name: "drep_tools", + arguments: { action: "alwaysAbstain" } + }) + expect(parseToolJson<{ drep: { tag: string } }>(drepAbstainResult).drep.tag).toBe("AlwaysAbstainDRep") + + // DRep alwaysNoConfidence + const drepNoConfResult = await client.callTool({ + name: "drep_tools", + arguments: { action: "alwaysNoConfidence" } + }) + expect(parseToolJson<{ drep: { tag: string } }>(drepNoConfResult).drep.tag).toBe("AlwaysNoConfidenceDRep") + + // ── Value tools ────────────────────────────────────────────────────── + const valueOnlyCoin = await client.callTool({ + name: "value_tools", + arguments: { action: "onlyCoin", lovelace: "5000000" } + }) + const vcResult = parseToolJson<{ cborHex: string; value: { coin: string } }>(valueOnlyCoin) + expect(vcResult.value.coin).toBe("5000000") + expect(vcResult.cborHex).toBeTruthy() + + const valueOnlyCoin2 = await client.callTool({ + name: "value_tools", + arguments: { action: "onlyCoin", lovelace: "3000000" } + }) + const vc2Hex = parseToolJson<{ cborHex: string }>(valueOnlyCoin2).cborHex + + const valueAdd = await client.callTool({ + name: "value_tools", + arguments: { action: "add", valueCborHex: vcResult.cborHex, valueCborHexB: vc2Hex } + }) + expect(parseToolJson<{ value: { coin: string } }>(valueAdd).value.coin).toBe("8000000") + + const valueGeq = await client.callTool({ + name: "value_tools", + arguments: { action: "geq", valueCborHex: vcResult.cborHex, valueCborHexB: vc2Hex } + }) + expect(parseToolJson<{ geq: boolean }>(valueGeq).geq).toBe(true) + + const valueGetAda = await client.callTool({ + name: "value_tools", + arguments: { action: "getAda", valueCborHex: vcResult.cborHex } + }) + expect(parseToolJson<{ lovelace: string }>(valueGetAda).lovelace).toBe("5000000") + + const valueIsAdaOnly = await client.callTool({ + name: "value_tools", + arguments: { action: "isAdaOnly", valueCborHex: vcResult.cborHex } + }) + expect(parseToolJson<{ isAdaOnly: boolean }>(valueIsAdaOnly).isAdaOnly).toBe(true) + + // ── Assets tools ───────────────────────────────────────────────────── + const assetsFromRecord = await client.callTool({ + name: "assets_tools", + arguments: { + action: "fromRecord", + record: { lovelace: "5000000", ["ab".repeat(28) + "cafe"]: "100" } + } + }) + const assetsResult = parseToolJson<{ assets: Record; cborHex: string }>(assetsFromRecord) + expect(assetsResult.assets["lovelace"]).toBe("5000000") + expect(assetsResult.cborHex).toBeTruthy() + + const assetsLovelace = await client.callTool({ + name: "assets_tools", + arguments: { action: "lovelaceOf", cborHex: assetsResult.cborHex } + }) + expect(parseToolJson<{ lovelace: string }>(assetsLovelace).lovelace).toBe("5000000") + + const assetsUnits = await client.callTool({ + name: "assets_tools", + arguments: { action: "getUnits", cborHex: assetsResult.cborHex } + }) + const units = parseToolJson<{ units: string[] }>(assetsUnits).units + expect(units).toContain("lovelace") + + const assetsHasMA = await client.callTool({ + name: "assets_tools", + arguments: { action: "hasMultiAsset", cborHex: assetsResult.cborHex } + }) + expect(parseToolJson<{ hasMultiAsset: boolean }>(assetsHasMA).hasMultiAsset).toBe(true) + + // merge two Assets + const assetsFromLov = await client.callTool({ + name: "assets_tools", + arguments: { action: "fromLovelace", lovelace: "1000000" } + }) + const lovHex = parseToolJson<{ cborHex: string }>(assetsFromLov).cborHex + const assetsMerge = await client.callTool({ + name: "assets_tools", + arguments: { action: "merge", cborHex: assetsResult.cborHex, cborHexB: lovHex } + }) + const mergedAssets = parseToolJson<{ assets: Record }>(assetsMerge).assets + expect(mergedAssets["lovelace"]).toBe("6000000") + + // CBOR round-trip + const assetsFromCbor = await client.callTool({ + name: "assets_tools", + arguments: { action: "fromCbor", cborHex: assetsResult.cborHex } + }) + expect(parseToolJson<{ assets: Record }>(assetsFromCbor).assets["lovelace"]).toBe("5000000") + + // ── Unit tools ─────────────────────────────────────────────────────── + const policyHex = "ab".repeat(28) + const unitFromUnit = await client.callTool({ + name: "unit_tools", + arguments: { action: "fromUnit", unit: policyHex + "cafe" } + }) + const unitDetails = parseToolJson<{ policyIdHex: string; assetNameHex: string; label: number | null }>(unitFromUnit) + expect(unitDetails.policyIdHex).toBe(policyHex) + expect(unitDetails.assetNameHex).toBe("cafe") + + const unitToLabel = await client.callTool({ + name: "unit_tools", + arguments: { action: "toLabel", label: 222 } + }) + const labelResult = parseToolJson<{ labelHex: string; label: number }>(unitToLabel) + expect(labelResult.label).toBe(222) + expect(labelResult.labelHex).toBeTruthy() + + const unitFromLabel = await client.callTool({ + name: "unit_tools", + arguments: { action: "fromLabel", labelHex: labelResult.labelHex } + }) + expect(parseToolJson<{ label: number | null }>(unitFromLabel).label).toBe(222) + + // ── Coin tools ─────────────────────────────────────────────────────── + const coinAdd = await client.callTool({ + name: "coin_tools", + arguments: { action: "add", a: "5000000", b: "3000000" } + }) + expect(parseToolJson<{ result: string }>(coinAdd).result).toBe("8000000") + + const coinSubtract = await client.callTool({ + name: "coin_tools", + arguments: { action: "subtract", a: "5000000", b: "3000000" } + }) + expect(parseToolJson<{ result: string }>(coinSubtract).result).toBe("2000000") + + const coinCompare = await client.callTool({ + name: "coin_tools", + arguments: { action: "compare", a: "5000000", b: "3000000" } + }) + expect(parseToolJson<{ result: number }>(coinCompare).result).toBe(1) + + const coinValidate = await client.callTool({ + name: "coin_tools", + arguments: { action: "validate", a: "5000000" } + }) + expect(parseToolJson<{ valid: boolean }>(coinValidate).valid).toBe(true) + + const coinMax = await client.callTool({ + name: "coin_tools", + arguments: { action: "maxCoinValue" } + }) + expect(parseToolJson<{ maxCoinValue: string }>(coinMax).maxCoinValue).toBe("18446744073709551615") + + // ── Network tools ──────────────────────────────────────────────────── + const netToId = await client.callTool({ + name: "network_tools", + arguments: { action: "toId", network: "Mainnet" } + }) + expect(parseToolJson<{ networkId: number }>(netToId).networkId).toBe(1) + + const netFromId = await client.callTool({ + name: "network_tools", + arguments: { action: "fromId", networkId: 0 } + }) + expect(parseToolJson<{ network: string }>(netFromId).network).toBe("Preview") + + const netValidate = await client.callTool({ + name: "network_tools", + arguments: { action: "validate", network: "Mainnet" } + }) + expect(parseToolJson<{ valid: boolean }>(netValidate).valid).toBe(true) + + // ── Data construction tools ────────────────────────────────────────── + const dataInt = await client.callTool({ + name: "data_construct", + arguments: { action: "int", value: "42" } + }) + const intCbor = parseToolJson<{ cborHex: string }>(dataInt).cborHex + expect(intCbor).toBeTruthy() + + const dataBytes = await client.callTool({ + name: "data_construct", + arguments: { action: "bytes", value: "deadbeef" } + }) + expect(parseToolJson<{ cborHex: string }>(dataBytes).cborHex).toBeTruthy() + + const dataConstr = await client.callTool({ + name: "data_construct", + arguments: { action: "constr", index: "0", fieldsCborHex: [intCbor] } + }) + const constrResult = parseToolJson<{ cborHex: string; fieldCount: number }>(dataConstr) + expect(constrResult.fieldCount).toBe(1) + + const dataList = await client.callTool({ + name: "data_construct", + arguments: { action: "list", fieldsCborHex: [intCbor, intCbor] } + }) + expect(parseToolJson<{ length: number }>(dataList).length).toBe(2) + + // match + const dataMatch = await client.callTool({ + name: "data_construct", + arguments: { action: "match", dataCborHex: constrResult.cborHex } + }) + expect(parseToolJson<{ type: string }>(dataMatch).type).toBe("constr") + + const dataIsInt = await client.callTool({ + name: "data_construct", + arguments: { action: "isInt", dataCborHex: intCbor } + }) + expect(parseToolJson<{ isInt: boolean }>(dataIsInt).isInt).toBe(true) + + const dataIsConstr = await client.callTool({ + name: "data_construct", + arguments: { action: "isConstr", dataCborHex: constrResult.cborHex } + }) + expect(parseToolJson<{ isConstr: boolean }>(dataIsConstr).isConstr).toBe(true) + + // ── Hash tools ─────────────────────────────────────────────────────── + // hashTransactionRaw — just hash some arbitrary bytes + const hashRaw = await client.callTool({ + name: "hash_tools", + arguments: { action: "hashTransactionRaw", cborHex: "deadbeef" } + }) + const hashResult = parseToolJson<{ transactionHash: string }>(hashRaw) + expect(hashResult.transactionHash).toHaveLength(64) + + // ── Mint tools ──────────────────────────────────────────────────────── + const mintSingleton = await client.callTool({ + name: "mint_tools", + arguments: { + action: "singleton", + policyIdHex: "ab".repeat(28), + assetNameHex: "cafe", + amount: "100" + } + }) + const mintResult = parseToolJson<{ cborHex: string }>(mintSingleton) + expect(mintResult.cborHex).toBeTruthy() + + // insert an additional asset + const mintInsert = await client.callTool({ + name: "mint_tools", + arguments: { + action: "insert", + mintCborHex: mintResult.cborHex, + policyIdHex: "ab".repeat(28), + assetNameHex: "beef", + amount: "-50" + } + }) + const mintInserted = parseToolJson<{ cborHex: string }>(mintInsert) + expect(mintInserted.cborHex).toBeTruthy() + + // get value + const mintGet = await client.callTool({ + name: "mint_tools", + arguments: { + action: "getByHex", + mintCborHex: mintInserted.cborHex, + policyIdHex: "ab".repeat(28), + assetNameHex: "cafe" + } + }) + const mintGetResult = parseToolJson<{ value: string }>(mintGet) + expect(mintGetResult.value).toBe("100") + + // policyCount + const mintCount = await client.callTool({ + name: "mint_tools", + arguments: { action: "policyCount", mintCborHex: mintInserted.cborHex } + }) + const mintCountResult = parseToolJson<{ count: number }>(mintCount) + expect(mintCountResult.count).toBe(1) + + // empty mint + const mintEmpty = await client.callTool({ + name: "mint_tools", + arguments: { action: "empty" } + }) + const emptyMintResult = parseToolJson<{ cborHex: string }>(mintEmpty) + expect(emptyMintResult.cborHex).toBeTruthy() + + // ── Withdrawals tools ───────────────────────────────────────────────── + const wSingleton = await client.callTool({ + name: "withdrawals_tools", + arguments: { + action: "singleton", + rewardAccountHex: "e0" + "00".repeat(28), + coin: "5000000" + } + }) + const wResult = parseToolJson<{ cborHex: string }>(wSingleton) + expect(wResult.cborHex).toBeTruthy() + + // size + const wSize = await client.callTool({ + name: "withdrawals_tools", + arguments: { action: "size", withdrawalsCborHex: wResult.cborHex } + }) + const wSizeResult = parseToolJson<{ size: number }>(wSize) + expect(wSizeResult.size).toBe(1) + + // entries + const wEntries = await client.callTool({ + name: "withdrawals_tools", + arguments: { action: "entries", withdrawalsCborHex: wResult.cborHex } + }) + const wEntriesResult = parseToolJson<{ entries: Array<{ rewardAccountHex: string; coin: string }> }>(wEntries) + expect(wEntriesResult.entries).toHaveLength(1) + expect(wEntriesResult.entries[0].coin).toBe("5000000") + + // isEmpty on empty + const wEmpty = await client.callTool({ + name: "withdrawals_tools", + arguments: { action: "empty" } + }) + const wEmptyResult = parseToolJson<{ cborHex: string }>(wEmpty) + const wIsEmpty = await client.callTool({ + name: "withdrawals_tools", + arguments: { action: "isEmpty", withdrawalsCborHex: wEmptyResult.cborHex } + }) + expect(parseToolJson<{ isEmpty: boolean }>(wIsEmpty).isEmpty).toBe(true) + + // ── Anchor tools ────────────────────────────────────────────────────── + const anchorCreate = await client.callTool({ + name: "anchor_tools", + arguments: { + action: "create", + url: "https://example.com/proposal.json", + dataHashHex: "ab".repeat(32) + } + }) + const anchorResult = parseToolJson<{ cborHex: string }>(anchorCreate) + expect(anchorResult.cborHex).toBeTruthy() + + // roundtrip + const anchorParse = await client.callTool({ + name: "anchor_tools", + arguments: { action: "fromCbor", anchorCborHex: anchorResult.cborHex } + }) + const anchorParsed = parseToolJson<{ url: string; dataHashHex: string; cborHex: string }>(anchorParse) + expect(anchorParsed.url).toBe("https://example.com/proposal.json") + + // ── Certificate tools ───────────────────────────────────────────────── + const stakeReg = await client.callTool({ + name: "certificate_tools", + arguments: { + action: "stakeRegistration", + credentialType: "keyHash", + credentialHashHex: "00".repeat(28) + } + }) + const stakeRegResult = parseToolJson<{ cborHex: string }>(stakeReg) + expect(stakeRegResult.cborHex).toBeTruthy() + + // stakeDelegation + const stakeDeleg = await client.callTool({ + name: "certificate_tools", + arguments: { + action: "stakeDelegation", + credentialType: "keyHash", + credentialHashHex: "00".repeat(28), + poolKeyHashHex: "00".repeat(28) + } + }) + expect(parseToolJson<{ cborHex: string }>(stakeDeleg).cborHex).toBeTruthy() + + // regCert (Conway) + const regCert = await client.callTool({ + name: "certificate_tools", + arguments: { + action: "regCert", + credentialType: "keyHash", + credentialHashHex: "00".repeat(28), + coin: "2000000" + } + }) + expect(parseToolJson<{ cborHex: string }>(regCert).cborHex).toBeTruthy() + + // voteDelegCert + const voteDelegCert = await client.callTool({ + name: "certificate_tools", + arguments: { + action: "voteDelegCert", + credentialType: "keyHash", + credentialHashHex: "00".repeat(28), + drepType: "alwaysAbstain" + } + }) + expect(parseToolJson<{ cborHex: string }>(voteDelegCert).cborHex).toBeTruthy() + + // fromCbor roundtrip + const certParse = await client.callTool({ + name: "certificate_tools", + arguments: { action: "fromCbor", certCborHex: stakeRegResult.cborHex } + }) + const certParsed = parseToolJson<{ tag: string; cborHex: string }>(certParse) + expect(certParsed.tag).toBe("StakeRegistration") + + // ── Redeemer tools ──────────────────────────────────────────────────── + // Build a Data.int(42) CBOR for use as redeemer data + const dataIntRaw = await client.callTool({ + name: "data_construct", + arguments: { action: "int", value: "42" } + }) + const dataIntCbor = parseToolJson<{ cborHex: string }>(dataIntRaw).cborHex + + const spendRedeemer = await client.callTool({ + name: "redeemer_tools", + arguments: { + action: "spend", + index: "0", + dataCborHex: dataIntCbor, + mem: "100000", + steps: "200000" + } + }) + const spendResult = parseToolJson<{ cborHex: string }>(spendRedeemer) + expect(spendResult.cborHex).toBeTruthy() + + const mintRedeemer = await client.callTool({ + name: "redeemer_tools", + arguments: { + action: "mint", + index: "0", + dataCborHex: dataIntCbor, + mem: "0", + steps: "0" + } + }) + expect(parseToolJson<{ cborHex: string }>(mintRedeemer).cborHex).toBeTruthy() + + // fromCbor roundtrip + const redeemerParse = await client.callTool({ + name: "redeemer_tools", + arguments: { action: "fromCbor", redeemerCborHex: spendResult.cborHex } + }) + const rdParsed = parseToolJson<{ isSpend: boolean; isMint: boolean }>(redeemerParse) + expect(rdParsed.isSpend).toBe(true) + expect(rdParsed.isMint).toBe(false) + + // ── Voting tools ────────────────────────────────────────────────────── + const singleVote = await client.callTool({ + name: "voting_tools", + arguments: { + action: "singleVote", + voterType: "drep", + drepType: "keyHash", + drepHashHex: "00".repeat(28), + govActionTxHashHex: "ab".repeat(32), + govActionIndex: "0", + vote: "yes" + } + }) + const voteResult = parseToolJson<{ cborHex: string }>(singleVote) + expect(voteResult.cborHex).toBeTruthy() + + // roundtrip + const voteParse = await client.callTool({ + name: "voting_tools", + arguments: { action: "fromCbor", votingCborHex: voteResult.cborHex } + }) + expect(parseToolJson<{ cborHex: string }>(voteParse).cborHex).toBe(voteResult.cborHex) + + // ── Script ref tools ────────────────────────────────────────────────── + const srFromHex = await client.callTool({ + name: "script_ref_tools", + arguments: { action: "fromHex", hex: "00".repeat(10) } + }) + const srResult = parseToolJson<{ hex: string; cborHex: string }>(srFromHex) + expect(srResult.hex).toBe("00".repeat(10)) + expect(srResult.cborHex).toBeTruthy() + + // roundtrip via fromCbor + const srParse = await client.callTool({ + name: "script_ref_tools", + arguments: { action: "fromCbor", cborHex: srResult.cborHex } + }) + const srParsed = parseToolJson<{ hex: string }>(srParse) + expect(srParsed.hex).toBe("00".repeat(10)) + + // ── Governance action tools ─────────────────────────────────────────── + const infoAction = await client.callTool({ + name: "governance_action_tools", + arguments: { action: "infoAction" } + }) + const infoResult = parseToolJson<{ cborHex: string; type: string }>(infoAction) + expect(infoResult.type).toBe("InfoAction") + expect(infoResult.cborHex).toBe("8106") + + const noConfAction = await client.callTool({ + name: "governance_action_tools", + arguments: { action: "noConfidenceAction" } + }) + const noConfResult = parseToolJson<{ cborHex: string; type: string }>(noConfAction) + expect(noConfResult.type).toBe("NoConfidenceAction") + expect(noConfResult.cborHex).toBe("8203f6") + + const paramChange = await client.callTool({ + name: "governance_action_tools", + arguments: { action: "parameterChangeAction" } + }) + const paramResult = parseToolJson<{ cborHex: string; type: string }>(paramChange) + expect(paramResult.type).toBe("ParameterChangeAction") + expect(paramResult.cborHex).toBeTruthy() + + const noConfWithRef = await client.callTool({ + name: "governance_action_tools", + arguments: { + action: "noConfidenceAction", + prevGovActionIdTransactionHashHex: "00".repeat(32), + prevGovActionIdIndex: 0 + } + }) + const noConfRefResult = parseToolJson<{ cborHex: string }>(noConfWithRef) + expect(noConfRefResult.cborHex).toContain("00".repeat(32)) + + const inspected = await client.callTool({ + name: "governance_action_tools", + arguments: { action: "inspect", cborHex: "8106" } + }) + expect(parseToolJson<{ type: string }>(inspected).type).toBe("InfoAction") + + const fromCborGA = await client.callTool({ + name: "governance_action_tools", + arguments: { action: "fromCbor", cborHex: "8203f6" } + }) + expect(parseToolJson<{ type: string }>(fromCborGA).type).toBe("NoConfidenceAction") + + const hardFork = await client.callTool({ + name: "governance_action_tools", + arguments: { + action: "hardForkInitiationAction", + protocolVersionMajor: 10, + protocolVersionMinor: 0 + } + }) + expect(parseToolJson<{ type: string }>(hardFork).type).toBe("HardForkInitiationAction") + + const newConst = await client.callTool({ + name: "governance_action_tools", + arguments: { + action: "newConstitutionAction", + anchorUrl: "https://example.com", + anchorDataHashHex: "00".repeat(32) + } + }) + expect(parseToolJson<{ type: string }>(newConst).type).toBe("NewConstitutionAction") + + const updateComm = await client.callTool({ + name: "governance_action_tools", + arguments: { + action: "updateCommitteeAction", + thresholdNumerator: 1, + thresholdDenominator: 2 + } + }) + expect(parseToolJson<{ type: string }>(updateComm).type).toBe("UpdateCommitteeAction") + + const treasuryWd = await client.callTool({ + name: "governance_action_tools", + arguments: { + action: "treasuryWithdrawalsAction", + withdrawals: [{ + rewardAccountHex: "e0" + "00".repeat(28), + coin: "1000000" + }] + } + }) + expect(parseToolJson<{ type: string }>(treasuryWd).type).toBe("TreasuryWithdrawalsAction") + + // ── Proposal tools ──────────────────────────────────────────────────── + const proposal = await client.callTool({ + name: "proposal_tools", + arguments: { + action: "create", + deposit: "500000000", + rewardAccountHex: "e0" + "00".repeat(28), + governanceActionCborHex: "8106", + anchorUrl: "https://example.com/proposal", + anchorDataHashHex: "00".repeat(32) + } + }) + const proposalResult = parseToolJson<{ cborHex: string }>(proposal) + expect(proposalResult.cborHex).toBeTruthy() + + const ppFromCbor = await client.callTool({ + name: "proposal_tools", + arguments: { action: "fromCbor", cborHex: proposalResult.cborHex } + }) + const ppParsed = parseToolJson<{ deposit: string; anchorUrl: string }>(ppFromCbor) + expect(ppParsed.deposit).toBe("500000000") + expect(ppParsed.anchorUrl).toBe("https://example.com/proposal") + + // ── Transaction output tools ────────────────────────────────────────── + const txOut = await client.callTool({ + name: "tx_output_tools", + arguments: { + action: "create", + addressBech32: "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp", + lovelace: "5000000" + } + }) + const txOutResult = parseToolJson<{ cborHex: string }>(txOut) + expect(txOutResult.cborHex).toBeTruthy() + + const txOutParsed = await client.callTool({ + name: "tx_output_tools", + arguments: { action: "fromCbor", cborHex: txOutResult.cborHex } + }) + expect(parseToolJson<{ type: string }>(txOutParsed).type).toBe("BabbageTransactionOutput") + + // with datum hash + const txOutDatum = await client.callTool({ + name: "tx_output_tools", + arguments: { + action: "create", + addressBech32: "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp", + lovelace: "5000000", + datumHashHex: "00".repeat(32) + } + }) + const txOutDatumResult = parseToolJson<{ cborHex: string }>(txOutDatum) + expect(txOutDatumResult.cborHex).toBeTruthy() + + const txOutDatumParsed = await client.callTool({ + name: "tx_output_tools", + arguments: { action: "fromCbor", cborHex: txOutDatumResult.cborHex } + }) + expect(parseToolJson<{ datumHashHex: string }>(txOutDatumParsed).datumHashHex).toBe("00".repeat(32)) + + // with inline datum + const txOutInline = await client.callTool({ + name: "tx_output_tools", + arguments: { + action: "create", + addressBech32: "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp", + lovelace: "5000000", + inlineDatumCborHex: "182a" + } + }) + const txOutInlineResult = parseToolJson<{ cborHex: string }>(txOutInline) + expect(txOutInlineResult.cborHex).toBeTruthy() + + const txOutInlineParsed = await client.callTool({ + name: "tx_output_tools", + arguments: { action: "fromCbor", cborHex: txOutInlineResult.cborHex } + }) + expect(parseToolJson<{ inlineDatumCborHex: string }>(txOutInlineParsed).inlineDatumCborHex).toBe("182a") + + // ── Plutus data codec tools ─────────────────────────────────────────── + const orEnc = await client.callTool({ + name: "plutus_data_codec_tools", + arguments: { + action: "encodeOutputReference", + transactionIdHex: "00".repeat(32), + outputIndex: 0 + } + }) + const orEncResult = parseToolJson<{ cborHex: string }>(orEnc) + expect(orEncResult.cborHex).toBeTruthy() + + const orDec = await client.callTool({ + name: "plutus_data_codec_tools", + arguments: { action: "decodeOutputReference", cborHex: orEncResult.cborHex } + }) + const orDecResult = parseToolJson<{ transactionIdHex: string; outputIndex: number }>(orDec) + expect(orDecResult.transactionIdHex).toBe("00".repeat(32)) + expect(orDecResult.outputIndex).toBe(0) + + const credEnc = await client.callTool({ + name: "plutus_data_codec_tools", + arguments: { + action: "encodeCredential", + credentialType: "VerificationKey", + credentialHashHex: "00".repeat(28) + } + }) + const credEncResult = parseToolJson<{ cborHex: string }>(credEnc) + expect(credEncResult.cborHex).toBeTruthy() + + const credDec = await client.callTool({ + name: "plutus_data_codec_tools", + arguments: { action: "decodeCredential", cborHex: credEncResult.cborHex } + }) + const credDecResult = parseToolJson<{ type: string; hashHex: string }>(credDec) + expect(credDecResult.type).toBe("VerificationKey") + expect(credDecResult.hashHex).toBe("00".repeat(28)) + + const addrEnc = await client.callTool({ + name: "plutus_data_codec_tools", + arguments: { + action: "encodeAddress", + credentialType: "VerificationKey", + credentialHashHex: "00".repeat(28), + stakeCredentialType: "VerificationKey", + stakeCredentialHashHex: "00".repeat(28) + } + }) + const addrEncResult = parseToolJson<{ cborHex: string }>(addrEnc) + expect(addrEncResult.cborHex).toBeTruthy() + + const addrDec = await client.callTool({ + name: "plutus_data_codec_tools", + arguments: { action: "decodeAddress", cborHex: addrEncResult.cborHex } + }) + const addrDecResult = parseToolJson<{ + paymentCredentialType: string; + stakeCredentialType: string + }>(addrDec) + expect(addrDecResult.paymentCredentialType).toBe("VerificationKey") + expect(addrDecResult.stakeCredentialType).toBe("VerificationKey") + + const lovEnc = await client.callTool({ + name: "plutus_data_codec_tools", + arguments: { action: "encodeLovelace", lovelace: "42000000" } + }) + const lovEncResult = parseToolJson<{ cborHex: string }>(lovEnc) + expect(lovEncResult.cborHex).toBeTruthy() + + const lovDec = await client.callTool({ + name: "plutus_data_codec_tools", + arguments: { action: "decodeLovelace", cborHex: lovEncResult.cborHex } + }) + expect(parseToolJson<{ lovelace: string }>(lovDec).lovelace).toBe("42000000") + + // ── pool_params_tools ───────────────────────────────────────────── + // Create a full pool params with relay + const poolParams = await client.callTool({ + name: "pool_params_tools", + arguments: { + action: "createPoolParams", + operatorHex: "aa".repeat(28), + vrfKeyHashHex: "bb".repeat(32), + pledge: "10000000000", + cost: "340000000", + marginNumerator: "1", + marginDenominator: "100", + rewardAccountHex: "e1" + "dd".repeat(28), + poolOwnerHexes: ["cc".repeat(28)], + relays: [{ type: "singleHostName", port: 3001, dnsName: "relay.example.com" }] + } + }) + const poolParamsResult = parseToolJson<{ cbor: string; json: any }>(poolParams) + expect(poolParamsResult.cbor).toBeTruthy() + expect(poolParamsResult.json._tag).toBe("PoolParams") + + // Create relay standalone + const relays = await client.callTool({ + name: "pool_params_tools", + arguments: { + action: "createRelay", + relays: [ + { type: "singleHostName", port: 6000, dnsName: "relay1.example.com" }, + { type: "multiHostName", dnsName: "pool.example.com" } + ] + } + }) + const relayResult = parseToolJson<{ relays: any[] }>(relays) + expect(relayResult.relays).toHaveLength(2) + expect(relayResult.relays[0].type).toBe("singleHostName") + expect(relayResult.relays[1].type).toBe("multiHostName") + + // Pool retirement certificate + const poolRetire = await client.callTool({ + name: "pool_params_tools", + arguments: { + action: "poolRetirement", + poolKeyHashHex: "aa".repeat(28), + epoch: "350" + } + }) + expect(parseToolJson<{ tag: string }>(poolRetire).tag).toBe("PoolRetirement") + + // fromCbor roundtrip + const poolFromCbor = await client.callTool({ + name: "pool_params_tools", + arguments: { + action: "fromCbor", + cborHex: poolParamsResult.cbor + } + }) + expect(parseToolJson<{ json: any }>(poolFromCbor).json._tag).toBe("PoolParams") + + // ── drep_cert_tools ───────────────────────────────────────────── + // Register DRep + const regDrep = await client.callTool({ + name: "drep_cert_tools", + arguments: { + action: "regDrep", + credentialType: "keyhash", + credentialHashHex: "dd".repeat(28), + coin: "500000000", + anchorUrl: "https://example.com/drep.json", + anchorDataHashHex: "ee".repeat(32) + } + }) + const regDrepResult = parseToolJson<{ tag: string; json: any }>(regDrep) + expect(regDrepResult.tag).toBe("RegDrepCert") + expect(regDrepResult.json.coin).toBe("500000000") + + // Unregister DRep + const unregDrep = await client.callTool({ + name: "drep_cert_tools", + arguments: { + action: "unregDrep", + credentialType: "keyhash", + credentialHashHex: "dd".repeat(28), + coin: "500000000" + } + }) + expect(parseToolJson<{ tag: string }>(unregDrep).tag).toBe("UnregDrepCert") + + // Update DRep + const updateDrep = await client.callTool({ + name: "drep_cert_tools", + arguments: { + action: "updateDrep", + credentialType: "scripthash", + credentialHashHex: "ff".repeat(28) + } + }) + expect(parseToolJson<{ tag: string }>(updateDrep).tag).toBe("UpdateDrepCert") + + // ── committee_cert_tools ───────────────────────────────────────── + // Authorize committee hot key + const authHot = await client.callTool({ + name: "committee_cert_tools", + arguments: { + action: "authHot", + coldCredentialType: "keyhash", + coldCredentialHashHex: "aa".repeat(28), + hotCredentialType: "keyhash", + hotCredentialHashHex: "bb".repeat(28) + } + }) + expect(parseToolJson<{ tag: string }>(authHot).tag).toBe("AuthCommitteeHotCert") + + // Resign committee cold credential + const resignCold = await client.callTool({ + name: "committee_cert_tools", + arguments: { + action: "resignCold", + coldCredentialType: "scripthash", + coldCredentialHashHex: "cc".repeat(28), + anchorUrl: "https://example.com/resign.json", + anchorDataHashHex: "dd".repeat(32) + } + }) + const resignResult = parseToolJson<{ tag: string; json: any }>(resignCold) + expect(resignResult.tag).toBe("ResignCommitteeColdCert") + expect(resignResult.json.anchor).not.toBeNull() + + // ── constitution_tools ───────────────────────────────────────── + // Create constitution + const constitution = await client.callTool({ + name: "constitution_tools", + arguments: { + action: "create", + anchorUrl: "https://example.com/constitution.json", + anchorDataHashHex: "ab".repeat(32) + } + }) + const constResult = parseToolJson<{ cbor: string; json: any }>(constitution) + expect(constResult.cbor).toBeTruthy() + expect(constResult.json._tag).toBe("Constitution") + expect(constResult.json.scriptHash).toBeNull() + + // Create with guardrail script + const constWithScript = await client.callTool({ + name: "constitution_tools", + arguments: { + action: "create", + anchorUrl: "https://example.com/constitution.json", + anchorDataHashHex: "ab".repeat(32), + scriptHashHex: "cd".repeat(28) + } + }) + expect(parseToolJson<{ json: any }>(constWithScript).json.scriptHash).not.toBeNull() + + // fromCbor roundtrip + const constFromCbor = await client.callTool({ + name: "constitution_tools", + arguments: { action: "fromCbor", cborHex: constResult.cbor } + }) + expect(parseToolJson<{ json: any }>(constFromCbor).json._tag).toBe("Constitution") + + // ── protocol_param_update_tools ───────────────────────────────── + // Create with basic fields + const ppuBasic = await client.callTool({ + name: "protocol_param_update_tools", + arguments: { + action: "create", + minfeeA: "44", + minfeeB: "155381", + maxTxSize: "16384", + keyDeposit: "2000000", + poolDeposit: "500000000" + } + }) + const ppuResult = parseToolJson<{ cbor: string; fieldsSet: string[] }>(ppuBasic) + expect(ppuResult.cbor).toBeTruthy() + expect(ppuResult.fieldsSet).toContain("minfeeA") + expect(ppuResult.fieldsSet).toContain("keyDeposit") + + // Create with ExUnits + const ppuExUnits = await client.callTool({ + name: "protocol_param_update_tools", + arguments: { + action: "create", + maxTxExMem: "14000000", + maxTxExSteps: "10000000000", + maxBlockExMem: "62000000", + maxBlockExSteps: "20000000000" + } + }) + const ppuExResult = parseToolJson<{ cbor: string; fieldsSet: string[] }>(ppuExUnits) + expect(ppuExResult.fieldsSet).toContain("maxTxExUnits") + expect(ppuExResult.fieldsSet).toContain("maxBlockExUnits") + + // Create with voting thresholds + const ui = { numerator: "1", denominator: "2" } + const ppuVoting = await client.callTool({ + name: "protocol_param_update_tools", + arguments: { + action: "create", + drepVotingThresholds: [ui, ui, ui, ui, ui, ui, ui, ui, ui, ui], + poolVotingThresholds: [ui, ui, ui, ui, ui] + } + }) + const ppuVotingResult = parseToolJson<{ cbor: string; fieldsSet: string[] }>(ppuVoting) + expect(ppuVotingResult.fieldsSet).toContain("drepVotingThresholds") + expect(ppuVotingResult.fieldsSet).toContain("poolVotingThresholds") + + // fromCbor roundtrip + const ppuFromCbor = await client.callTool({ + name: "protocol_param_update_tools", + arguments: { action: "fromCbor", cborHex: ppuResult.cbor } + }) + expect(parseToolJson<{ fields: any }>(ppuFromCbor).fields).toBeTruthy() + + // ── transaction_input_tools ──────────────────────────────────── + const txInBuild = await client.callTool({ + name: "transaction_input_tools", + arguments: { + action: "build", + txHashHex: "a".repeat(64), + index: 0 + } + }) + const txInResult = parseToolJson<{ cbor: string; json: any }>(txInBuild) + expect(txInResult.cbor).toBeTruthy() + + // roundtrip fromCbor + const txInDecoded = await client.callTool({ + name: "transaction_input_tools", + arguments: { action: "fromCbor", cborHex: txInResult.cbor } + }) + expect(parseToolJson<{ json: any }>(txInDecoded).json).toBeTruthy() + + // toCbor + const txInToCbor = await client.callTool({ + name: "transaction_input_tools", + arguments: { action: "toCbor", txHashHex: "b".repeat(64), index: 1 } + }) + expect(parseToolJson<{ cbor: string }>(txInToCbor).cbor).toBeTruthy() + + // ── transaction_body_tools ────────────────────────────────────── + const tbBuild = await client.callTool({ + name: "transaction_body_tools", + arguments: { + action: "build", + inputs: [{ txHashHex: "a".repeat(64), index: 0 }], + outputs: [], + fee: "200000" + } + }) + const tbResult = parseToolJson<{ cbor: string }>(tbBuild) + expect(tbResult.cbor).toBeTruthy() + + // fromCbor roundtrip + const tbDecoded = await client.callTool({ + name: "transaction_body_tools", + arguments: { action: "fromCbor", cborHex: tbResult.cbor } + }) + expect(parseToolJson<{ json: any }>(tbDecoded).json).toBeTruthy() + + // build with optional fields + const tbWithTtl = await client.callTool({ + name: "transaction_body_tools", + arguments: { + action: "build", + inputs: [{ txHashHex: "c".repeat(64), index: 2 }], + outputs: [], + fee: "300000", + ttl: "100000", + networkId: 1 + } + }) + expect(parseToolJson<{ cbor: string }>(tbWithTtl).cbor).toBeTruthy() + + // ── pointer_address_tools ─────────────────────────────────────── + const ptrBuild = await client.callTool({ + name: "pointer_address_tools", + arguments: { action: "buildPointer", slot: 100, txIndex: 1, certIndex: 1 } + }) + expect(parseToolJson<{ json: any }>(ptrBuild).json).toBeTruthy() + + // build full PointerAddress + const ptrAddrBuild = await client.callTool({ + name: "pointer_address_tools", + arguments: { + action: "buildAddress", + slot: 100, + txIndex: 1, + certIndex: 1, + networkId: 1, + paymentCredentialType: "keyhash", + paymentCredentialHashHex: "a".repeat(56) + } + }) + const ptrAddrResult = parseToolJson<{ hex: string; json: any }>(ptrAddrBuild) + expect(ptrAddrResult.hex).toBeTruthy() + + // ── plutus_value_tools ────────────────────────────────────────── + const pvAdaOnly = await client.callTool({ + name: "plutus_value_tools", + arguments: { action: "buildAdaOnly", lovelace: "2000000" } + }) + const pvAdaResult = parseToolJson<{ cbor: string }>(pvAdaOnly) + expect(pvAdaResult.cbor).toBeTruthy() + + // decode roundtrip + const pvDecoded = await client.callTool({ + name: "plutus_value_tools", + arguments: { action: "decode", cborHex: pvAdaResult.cbor } + }) + const pvPolicies = parseToolJson<{ policies: any[] }>(pvDecoded).policies + expect(pvPolicies.length).toBeGreaterThan(0) + + // multi-asset + const pvMulti = await client.callTool({ + name: "plutus_value_tools", + arguments: { + action: "buildMultiAsset", + lovelace: "5000000", + assets: [{ policyIdHex: "a".repeat(56), assetNameHex: "ff", amount: "100" }] + } + }) + expect(parseToolJson<{ cbor: string }>(pvMulti).cbor).toBeTruthy() + + // ── script_tools ──────────────────────────────────────────────── + // First build a native script to wrap + const nsForWrap = await client.callTool({ + name: "native_script_tools", + arguments: { + action: "build", + spec: { tag: "pubKey", keyHashHex: "a".repeat(56) } + } + }) + const nsWrapCbor = parseToolJson<{ cborHex: string }>(nsForWrap).cborHex + + const scriptWrap = await client.callTool({ + name: "script_tools", + arguments: { action: "wrapNativeScript", nativeScriptCborHex: nsWrapCbor } + }) + const scriptWrapResult = parseToolJson<{ scriptCbor: string }>(scriptWrap) + expect(scriptWrapResult.scriptCbor).toBeTruthy() + + // hashScript + const scriptHash = await client.callTool({ + name: "script_tools", + arguments: { action: "hashScript", scriptCborHex: scriptWrapResult.scriptCbor } + }) + expect(parseToolJson<{ scriptHash: string }>(scriptHash).scriptHash).toBeTruthy() + + // ── bip32_key_tools ───────────────────────────────────────────── + const entropy24 = "a".repeat(48) // 24 bytes + const bip32Root = await client.callTool({ + name: "bip32_key_tools", + arguments: { action: "fromEntropy", entropyHex: entropy24 } + }) + const rootKeyHex = parseToolJson<{ bip32PrivateKeyHex: string }>(bip32Root).bip32PrivateKeyHex + expect(rootKeyHex).toBeTruthy() + expect(rootKeyHex.length).toBe(192) // 96 bytes + + // derivePath + const bip32Derived = await client.callTool({ + name: "bip32_key_tools", + arguments: { action: "derivePath", bip32KeyHex: rootKeyHex, path: "m/1852'/1815'/0'/0/0" } + }) + expect(parseToolJson<{ derivedKeyHex: string }>(bip32Derived).derivedKeyHex).toBeTruthy() + + // toPrivateKey + const bip32Priv = await client.callTool({ + name: "bip32_key_tools", + arguments: { action: "toPrivateKey", bip32KeyHex: rootKeyHex } + }) + expect(parseToolJson<{ privateKeyHex: string }>(bip32Priv).privateKeyHex).toBeTruthy() + + // toPublicKey + const bip32Pub = await client.callTool({ + name: "bip32_key_tools", + arguments: { action: "toPublicKey", bip32KeyHex: rootKeyHex } + }) + const pubResult = parseToolJson<{ bip32PublicKeyHex: string; rawPublicKeyHex: string }>(bip32Pub) + expect(pubResult.bip32PublicKeyHex).toBeTruthy() + expect(pubResult.rawPublicKeyHex).toBeTruthy() + + // toXPRV / fromXPRV roundtrip + const bip32Xprv = await client.callTool({ + name: "bip32_key_tools", + arguments: { action: "toXPRV", bip32KeyHex: rootKeyHex } + }) + const xprvHex = parseToolJson<{ xprvHex: string }>(bip32Xprv).xprvHex + expect(xprvHex).toBeTruthy() + + const bip32FromXprv = await client.callTool({ + name: "bip32_key_tools", + arguments: { action: "fromXPRV", xprvHex } + }) + expect(parseToolJson<{ bip32PrivateKeyHex: string }>(bip32FromXprv).bip32PrivateKeyHex).toBe(rootKeyHex) + + // inspect + const bip32Inspect = await client.callTool({ + name: "bip32_key_tools", + arguments: { action: "inspect", bip32KeyHex: rootKeyHex } + }) + expect(parseToolJson<{ ed25519PrivateKeyHex: string }>(bip32Inspect).ed25519PrivateKeyHex).toBeTruthy() + + // ── byron_address_tools ───────────────────────────────────────── + // Byron addresses are hard to construct but we can test the tool exists + // and the fromHex path at least accepts the call shape + + // ── uplc_tools ────────────────────────────────────────────────── + const uplcDetect = await client.callTool({ + name: "uplc_tools", + arguments: { action: "detectEncoding", scriptHex: "480100003322220051" } + }) + expect(parseToolJson<{ encodingLevel: string }>(uplcDetect).encodingLevel).toBeTruthy() + + // doubleEncode / singleEncode + const singleHex = "480100003322220051" + const uplcDouble = await client.callTool({ + name: "uplc_tools", + arguments: { action: "doubleEncode", scriptHex: singleHex } + }) + expect(parseToolJson<{ doubleCborHex: string }>(uplcDouble).doubleCborHex).toBeTruthy() + + // ── ed25519_signature_tools ───────────────────────────────────── + const sigHex = "a".repeat(128) // 64 bytes + const sigValidate = await client.callTool({ + name: "ed25519_signature_tools", + arguments: { action: "validate", signatureHex: sigHex } + }) + expect(parseToolJson<{ valid: boolean }>(sigValidate).valid).toBe(true) + + const sigFromHex = await client.callTool({ + name: "ed25519_signature_tools", + arguments: { action: "fromHex", signatureHex: sigHex } + }) + const sigResult = parseToolJson<{ hex: string; bytesLength: number }>(sigFromHex) + expect(sigResult.hex).toBe(sigHex) + expect(sigResult.bytesLength).toBe(64) + + // ── redeemers_collection_tools ────────────────────────────────── + const redeemersCol = await client.callTool({ + name: "redeemers_collection_tools", + arguments: { + action: "build", + redeemers: [{ + tag: "spend", + index: 0, + dataCborHex: "d87980", // Constr(0, []) + exUnitsMem: "1000000", + exUnitsSteps: "2000000" + }] + } + }) + const redeemersResult = parseToolJson<{ cborHex: string; count: number }>(redeemersCol) + expect(redeemersResult.cborHex).toBeTruthy() + expect(redeemersResult.count).toBe(1) + + // fromCbor roundtrip + const redeemersDecoded = await client.callTool({ + name: "redeemers_collection_tools", + arguments: { action: "fromCbor", cborHex: redeemersResult.cborHex } + }) + expect(parseToolJson<{ decoded: any }>(redeemersDecoded).decoded).toBeTruthy() + + // ── proposal_procedures_collection_tools ──────────────────────── + // This tool decodes existing CBOR, so test with the encode from proposal_tools + // (PP collection encoding requires fully valid ProposalProcedure objects) + + // Verify all tools are listed + const allTools = await client.listTools() + const toolNames = allTools.tools.map((t) => t.name) + expect(toolNames).toContain("evaluator_info") + expect(toolNames).toContain("time_slot_convert") + expect(toolNames).toContain("blueprint_parse") + expect(toolNames).toContain("blueprint_codegen") + expect(toolNames).toContain("message_sign") + expect(toolNames).toContain("message_verify") + expect(toolNames).toContain("fee_validate") + expect(toolNames).toContain("encoding_codec") + expect(toolNames).toContain("client_attach") + expect(toolNames).toContain("result_call") + expect(toolNames).toContain("key_generate") + expect(toolNames).toContain("native_script_tools") + expect(toolNames).toContain("utxo_tools") + expect(toolNames).toContain("address_build") + expect(toolNames).toContain("metadata_tools") + expect(toolNames).toContain("credential_tools") + expect(toolNames).toContain("drep_tools") + expect(toolNames).toContain("value_tools") + expect(toolNames).toContain("assets_tools") + expect(toolNames).toContain("unit_tools") + expect(toolNames).toContain("coin_tools") + expect(toolNames).toContain("network_tools") + expect(toolNames).toContain("data_construct") + expect(toolNames).toContain("hash_tools") + expect(toolNames).toContain("mint_tools") + expect(toolNames).toContain("withdrawals_tools") + expect(toolNames).toContain("anchor_tools") + expect(toolNames).toContain("certificate_tools") + expect(toolNames).toContain("redeemer_tools") + expect(toolNames).toContain("voting_tools") + expect(toolNames).toContain("script_ref_tools") + expect(toolNames).toContain("governance_action_tools") + expect(toolNames).toContain("proposal_tools") + expect(toolNames).toContain("tx_output_tools") + expect(toolNames).toContain("plutus_data_codec_tools") + expect(toolNames).toContain("pool_params_tools") + expect(toolNames).toContain("drep_cert_tools") + expect(toolNames).toContain("committee_cert_tools") + expect(toolNames).toContain("constitution_tools") + expect(toolNames).toContain("protocol_param_update_tools") + expect(toolNames).toContain("transaction_input_tools") + expect(toolNames).toContain("transaction_body_tools") + expect(toolNames).toContain("pointer_address_tools") + expect(toolNames).toContain("plutus_value_tools") + expect(toolNames).toContain("script_tools") + expect(toolNames).toContain("bip32_key_tools") + expect(toolNames).toContain("byron_address_tools") + expect(toolNames).toContain("uplc_tools") + expect(toolNames).toContain("ed25519_signature_tools") + expect(toolNames).toContain("redeemers_collection_tools") + expect(toolNames).toContain("proposal_procedures_collection_tools") + expect(toolNames).toContain("devnet") + + await client.close() + await transport.close() + + server.close() + await once(server, "close") + }) +}) \ No newline at end of file diff --git a/packages/evolution-mcp/tsconfig.build.json b/packages/evolution-mcp/tsconfig.build.json new file mode 100644 index 00000000..dd99b13a --- /dev/null +++ b/packages/evolution-mcp/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./tsconfig.src.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "dist", + "types": ["node"], + "rootDir": "src", + "stripInternal": true + }, + "exclude": ["test"] +} \ No newline at end of file diff --git a/packages/evolution-mcp/tsconfig.json b/packages/evolution-mcp/tsconfig.json new file mode 100644 index 00000000..994650bf --- /dev/null +++ b/packages/evolution-mcp/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../tsconfig.base.json", + "include": [], + "references": [{ "path": "tsconfig.src.json" }] +} \ No newline at end of file diff --git a/packages/evolution-mcp/tsconfig.src.json b/packages/evolution-mcp/tsconfig.src.json new file mode 100644 index 00000000..9e01a63d --- /dev/null +++ b/packages/evolution-mcp/tsconfig.src.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../tsconfig.base.json", + "include": ["src", "test"], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "outDir": ".tsbuildinfo/src", + "types": ["node", "vitest/globals"], + "rootDir": "." + } +} \ No newline at end of file diff --git a/packages/evolution-mcp/vitest.config.ts b/packages/evolution-mcp/vitest.config.ts new file mode 100644 index 00000000..2fc14a4e --- /dev/null +++ b/packages/evolution-mcp/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + globals: true, + environment: "node", + testTimeout: 30_000, + hookTimeout: 30_000, + teardownTimeout: 15_000, + exclude: ["**/node_modules/**", "**/dist/**", "**/.turbo/**", "**/.tsbuildinfo/**"] + } +}) \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e01fb1ff..ea500333 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -332,6 +332,43 @@ importers: specifier: ^5.9.2 version: 5.9.2 + packages/evolution-mcp: + dependencies: + '@effect/platform': + specifier: ^0.90.10 + version: 0.90.10(effect@3.19.3) + '@effect/platform-node': + specifier: ^0.96.1 + version: 0.96.1(@effect/cluster@0.48.2(@effect/platform@0.90.10(effect@3.19.3))(@effect/rpc@0.69.1(@effect/platform@0.90.10(effect@3.19.3))(effect@3.19.3))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.19.3))(effect@3.19.3))(@effect/platform@0.90.10(effect@3.19.3))(effect@3.19.3))(@effect/workflow@0.9.2(@effect/platform@0.90.10(effect@3.19.3))(@effect/rpc@0.69.1(@effect/platform@0.90.10(effect@3.19.3))(effect@3.19.3))(effect@3.19.3))(effect@3.19.3))(@effect/platform@0.90.10(effect@3.19.3))(@effect/rpc@0.69.1(@effect/platform@0.90.10(effect@3.19.3))(effect@3.19.3))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.19.3))(effect@3.19.3))(@effect/platform@0.90.10(effect@3.19.3))(effect@3.19.3))(bufferutil@4.1.0)(effect@3.19.3)(utf-8-validate@6.0.6) + '@evolution-sdk/aiken-uplc': + specifier: workspace:* + version: link:../aiken-uplc + '@evolution-sdk/devnet': + specifier: workspace:* + version: link:../evolution-devnet + '@evolution-sdk/evolution': + specifier: workspace:* + version: link:../evolution + '@evolution-sdk/scalus-uplc': + specifier: workspace:* + version: link:../scalus-uplc + '@modelcontextprotocol/sdk': + specifier: ^1.27.1 + version: 1.27.1(zod@4.1.12) + effect: + specifier: ^3.19.3 + version: 3.19.3 + zod: + specifier: ^4.1.11 + version: 4.1.12 + devDependencies: + tsx: + specifier: ^4.20.4 + version: 4.20.4 + typescript: + specifier: ^5.9.2 + version: 5.9.2 + packages/scalus-uplc: dependencies: '@evolution-sdk/evolution': @@ -993,6 +1030,12 @@ packages: engines: {node: '>=6'} hasBin: true + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1224,6 +1267,16 @@ packages: '@mermaid-js/parser@0.6.3': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} cpu: [arm64] @@ -2579,6 +2632,10 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2605,9 +2662,20 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + algoliasearch@5.43.0: resolution: {integrity: sha512-hbkK41JsuGYhk+atBDxlcKxskjDCh3OOEDpdKZPtw+3zucBqhlojRG5e5KtCmByGyYvwZswVeaSWglgLn2fibg==} engines: {node: '>= 14.0.0'} @@ -2887,6 +2955,10 @@ packages: block-iterator@1.1.2: resolution: {integrity: sha512-yAHUP44v2K25xLPdrgVTgwtuQctlullzjczu9CoUZom5AP3g4p1R1+aWHjS1GHG9JtcSUVUnbEPiuXiW5YZ24w==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -2930,6 +3002,10 @@ packages: resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==} engines: {node: '>=10.0.0'} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -3180,9 +3256,25 @@ packages: constant-case@2.0.0: resolution: {integrity: sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ==} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} @@ -3192,6 +3284,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -3490,6 +3586,10 @@ packages: delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dependency-tree@11.2.0: resolution: {integrity: sha512-+C1H3mXhcvMCeu5i2Jpg9dc0N29TWTuT6vJD7mHLAfVmAbo9zW8NlkvQ1tYd3PDMab0IRQM0ccoyX68EZtx9xw==} engines: {node: '>=18'} @@ -3508,10 +3608,6 @@ packages: engines: {node: '>=0.10'} hasBin: true - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} - engines: {node: '>=8'} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -3616,6 +3712,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + effect@3.19.3: resolution: {integrity: sha512-LodiPXiyUJWQ5LoMhUGbu0acD2ff5A5teJtUlLKDPVfoeWEBcZLlzK8BeVXpVa0f30UsdHouVCf0C/E0TxYMrA==} @@ -3631,6 +3730,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -3856,6 +3959,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -3863,6 +3970,14 @@ packages: events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -3883,6 +3998,16 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.3.1: + resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -3926,6 +4051,9 @@ packages: fast-readable-async-iterator@2.0.0: resolution: {integrity: sha512-8Sld+DuyWRIftl86ZguJxR2oXCBccOiJxrY/Rj9/7ZBynW8pYMWzIcqxFL1da+25jaWJZVa+HHX/8SsA21JdTA==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -3967,6 +4095,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-my-way-ts@0.1.6: resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} @@ -4001,12 +4133,20 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} freelist@1.0.3: resolution: {integrity: sha512-Ji7fEnMdZDGbS5oXElpRJsn9jPvBR8h/037D3bzreNmS8809cISq/2D9//JbA/TaZmkkN8cmecXwmQHmM+NHhg==} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-chunk-store@5.0.0: resolution: {integrity: sha512-tKlT0joU9KmsLn0dTbVYVUa7VNqYQhl0X2qPPsN9lPEc3guXOmQJWY5/7kpo34Sk273qyWT5mqEhROCQPF+JKw==} engines: {node: '>=12.20.0'} @@ -4313,12 +4453,20 @@ packages: header-case@1.0.1: resolution: {integrity: sha512-i0q9mkOeSuhXw6bGgiQCCBgY/jlZuV/7dZXyZ9c6LcBrqwvT8eT719E9uxE5LiZftdl+z81Ugbg/VvXV4OJOeQ==} + hono@4.12.8: + resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} + engines: {node: '>=16.9.0'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-parser-js@0.4.13: resolution: {integrity: sha512-u8u5ZaG0Tr/VvHlucK2ufMuOp4/5bvwgneXle+y228K5rMbJOlVjThONcaAw3ikAy8b2OO9RfEucdMHFz3UWMA==} @@ -4350,6 +4498,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -4417,12 +4569,20 @@ packages: resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} engines: {node: '>= 12'} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + ip-set@2.2.0: resolution: {integrity: sha512-NmmY3BfY4pejh6GOqNcNWRsBNdR+I7pUVtXRgZlkZdcnLtlG4X6HNtu2FZoCGyvGRzyroP1fJ+SJZBZ65JJl/Q==} ip@2.0.1: resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + ipaddr.js@2.3.0: resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} engines: {node: '>= 10'} @@ -4563,6 +4723,9 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -4688,6 +4851,9 @@ packages: join-async-iterator@1.1.1: resolution: {integrity: sha512-ATse+nuNeKZ9K1y27LKdvPe/GCe9R/u9dw9vI248e+vILeRK3IcJP4JUPAlSmKRCDK0cKhEwfmiw4Skqx7UnGQ==} + jose@6.2.1: + resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4713,6 +4879,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -5077,9 +5249,17 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memory-chunk-store@1.3.5: resolution: {integrity: sha512-E1Xc1U4ifk/FkC2ZsWhCaW1xg9HbE/OBmQTLe2Tr9c27YPSLbW7kw1cnb3kQWD1rDtErFJHa7mB9EVrs7aTx9g==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -5199,6 +5379,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -5459,6 +5647,10 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -5567,6 +5759,10 @@ packages: engines: {node: '>=12.20.0'} hasBin: true + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + pascal-case@2.0.1: resolution: {integrity: sha512-qjS4s8rBOJa2Xm0jmxXiyh1+OFf6ekCWOvUaRgAQSktzlTbMotS0nmG9gyYAybCWBcuP4fsBeRCKNwGBnMe2OQ==} @@ -5646,6 +5842,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -5773,6 +5973,10 @@ packages: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + proxy-agent@6.5.0: resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} engines: {node: '>= 14'} @@ -5797,6 +6001,10 @@ packages: resolution: {integrity: sha512-XyQCIXux1zEIA3NPb0AeR8UMYvXZzWEhgdBgBjH9gO7M48H9uoHzviNz8pXw3UzrAcxRRRn9gxHewAVK7bn9qw==} hasBin: true + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -5829,6 +6037,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + rc4@0.1.5: resolution: {integrity: sha512-xdDTNV90z5x5u25Oc871Xnvu7yAr4tV7Eluh0VSvrhUkry39q1k+zkz7xroqHbRq+8PiazySHJPArqifUvz9VA==} engines: {node: '>=0.10.0'} @@ -6011,6 +6223,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + requirejs-config-file@4.0.0: resolution: {integrity: sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==} engines: {node: '>=10.13.0'} @@ -6069,6 +6285,10 @@ packages: roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -6160,9 +6380,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + sentence-case@2.1.1: resolution: {integrity: sha512-ENl7cYHaK/Ktwk5OTD+aDbQ3uC8IByu/6Bkg+HDv8Mm+XnBnppVNalcfJTNsp1ibstKh030/JKQQWglDvtKwEQ==} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -6179,6 +6407,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} @@ -6293,6 +6524,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -6559,6 +6794,10 @@ packages: toggle-selection@1.0.6: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + torrent-discovery@11.0.19: resolution: {integrity: sha512-BLhdj7o0px+u72UuhJmq6CB0LBkZOa1nwgbd5ktyTELJlvcRL8EoxSSmSpzMOIScLGgslh1uLaAy/POhLpagtg==} engines: {node: '>=16.0.0'} @@ -6684,6 +6923,10 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -6771,6 +7014,10 @@ packages: unordered-set@2.0.1: resolution: {integrity: sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -6853,6 +7100,10 @@ packages: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -7087,6 +7338,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod@4.1.12: resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} @@ -7898,6 +8154,10 @@ snapshots: protobufjs: 7.5.4 yargs: 17.7.2 + '@hono/node-server@1.19.11(hono@4.12.8)': + dependencies: + hono: 4.12.8 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -8131,6 +8391,28 @@ snapshots: dependencies: langium: 3.3.1 + '@modelcontextprotocol/sdk@1.27.1(zod@4.1.12)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.8) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.1(express@5.2.1) + hono: 4.12.8 + jose: 6.2.1 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.1.12 + zod-to-json-schema: 3.25.1(zod@4.1.12) + transitivePeerDependencies: + - supports-color + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': optional: true @@ -8869,7 +9151,7 @@ snapshots: '@tailwindcss/oxide@4.1.12': dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 tar: 7.4.3 optionalDependencies: '@tailwindcss/oxide-android-arm64': 4.1.12 @@ -9271,7 +9553,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.40.0(typescript@5.9.2) '@typescript-eslint/types': 8.40.0 - debug: 4.4.1 + debug: 4.4.3 typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -9290,7 +9572,7 @@ snapshots: '@typescript-eslint/types': 8.40.0 '@typescript-eslint/typescript-estree': 8.40.0(typescript@5.9.2) '@typescript-eslint/utils': 8.40.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - debug: 4.4.1 + debug: 4.4.3 eslint: 9.34.0(jiti@2.5.1) ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 @@ -9305,7 +9587,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.40.0(typescript@5.9.2) '@typescript-eslint/types': 8.40.0 '@typescript-eslint/visitor-keys': 8.40.0 - debug: 4.4.1 + debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -9515,6 +9797,11 @@ snapshots: dependencies: event-target-shim: 5.0.1 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -9534,6 +9821,10 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -9541,6 +9832,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + algoliasearch@5.43.0: dependencies: '@algolia/abtesting': 1.9.0 @@ -9866,6 +10164,20 @@ snapshots: block-iterator@1.1.2: {} + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -9923,6 +10235,8 @@ snapshots: buildcheck@0.0.6: optional: true + bytes@3.1.2: {} + cac@6.7.14: {} cache-chunk-store@3.2.2: @@ -10179,8 +10493,16 @@ snapshots: snake-case: 2.1.0 upper-case: 1.1.3 + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + copy-to-clipboard@3.3.3: dependencies: toggle-selection: 1.0.6 @@ -10189,6 +10511,11 @@ snapshots: core-util-is@1.0.3: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -10524,6 +10851,8 @@ snapshots: dependencies: robust-predicates: 3.0.2 + depd@2.0.0: {} + dependency-tree@11.2.0: dependencies: commander: 12.1.0 @@ -10539,8 +10868,6 @@ snapshots: detect-libc@1.0.3: {} - detect-libc@2.0.4: {} - detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -10664,6 +10991,8 @@ snapshots: eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + effect@3.19.3: dependencies: '@standard-schema/spec': 1.0.0 @@ -10677,6 +11006,8 @@ snapshots: emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -11029,6 +11360,8 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + event-target-shim@5.0.1: {} events-universal@1.0.1: @@ -11037,6 +11370,12 @@ snapshots: transitivePeerDependencies: - bare-abort-controller + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -11069,6 +11408,44 @@ snapshots: expect-type@1.2.2: {} + express-rate-limit@8.3.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.7: {} extend-shallow@2.0.1: @@ -11109,6 +11486,8 @@ snapshots: fast-readable-async-iterator@2.0.0: {} + fast-uri@3.1.0: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -11158,6 +11537,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-my-way-ts@0.1.6: {} find-up@4.1.0: @@ -11192,10 +11582,14 @@ snapshots: dependencies: fetch-blob: 3.2.0 + forwarded@0.2.0: {} + fraction.js@5.3.4: {} freelist@1.0.3: {} + fresh@2.0.0: {} + fs-chunk-store@5.0.0(bare-url@2.3.2): dependencies: filename-reserved-regex: 3.0.0 @@ -11628,10 +12022,20 @@ snapshots: no-case: 2.3.2 upper-case: 1.1.3 + hono@4.12.8: {} + html-escaper@2.0.2: {} html-void-elements@3.0.0: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-parser-js@0.4.13: {} http-proxy-agent@7.0.2: @@ -11662,6 +12066,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -11742,12 +12150,16 @@ snapshots: ip-address@10.0.1: {} + ip-address@10.1.0: {} + ip-set@2.2.0: dependencies: ip: 2.0.1 ip@2.0.1: {} + ipaddr.js@1.9.1: {} + ipaddr.js@2.3.0: {} is-alphabetical@2.0.1: {} @@ -11873,6 +12285,8 @@ snapshots: dependencies: isobject: 3.0.1 + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -11985,6 +12399,8 @@ snapshots: join-async-iterator@1.1.1: {} + jose@6.2.1: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -12004,6 +12420,10 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -12463,10 +12883,14 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + media-typer@1.1.0: {} + memory-chunk-store@1.3.5: dependencies: queue-microtask: 1.2.3 + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -12765,6 +13189,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mime@3.0.0: {} mimic-fn@2.1.0: {} @@ -13014,6 +13444,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -13159,6 +13593,8 @@ snapshots: queue-microtask: 1.2.3 uint8-util: 2.2.6 + parseurl@1.3.3: {} + pascal-case@2.0.1: dependencies: camel-case: 3.0.0 @@ -13214,6 +13650,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -13368,6 +13806,11 @@ snapshots: '@types/node': 24.3.0 long: 5.3.2 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 @@ -13396,6 +13839,10 @@ snapshots: qrcode-svg@1.1.0: {} + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + quansync@0.2.11: {} queue-microtask@1.2.3: {} @@ -13438,6 +13885,13 @@ snapshots: range-parser@1.2.1: {} + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + rc4@0.1.5: {} rc@1.2.8: @@ -13693,6 +14147,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + requirejs-config-file@4.0.0: dependencies: esprima: 4.0.1 @@ -13765,6 +14221,16 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-async@2.4.1: {} run-parallel-limit@1.1.0: @@ -13843,11 +14309,36 @@ snapshots: semver@7.7.3: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + sentence-case@2.1.1: dependencies: no-case: 2.3.2 upper-case-first: 1.1.2 + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -13874,6 +14365,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setprototypeof@1.2.0: {} + shallowequal@1.1.0: {} sharp@0.34.5: @@ -14031,6 +14524,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.9.0: {} stop-iteration-iterator@1.1.0: @@ -14351,6 +14846,8 @@ snapshots: toggle-selection@1.0.6: {} + toidentifier@1.0.1: {} + torrent-discovery@11.0.19: dependencies: bittorrent-dht: 11.0.11 @@ -14481,6 +14978,12 @@ snapshots: type-fest@0.21.3: {} + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -14591,6 +15094,8 @@ snapshots: unordered-set@2.0.1: optional: true + unpipe@1.0.0: {} + unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.3 @@ -14696,6 +15201,8 @@ snapshots: validate-npm-package-name@5.0.1: {} + vary@1.1.2: {} + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -15005,6 +15512,10 @@ snapshots: yocto-queue@0.1.0: {} + zod-to-json-schema@3.25.1(zod@4.1.12): + dependencies: + zod: 4.1.12 + zod@4.1.12: {} zwitch@2.0.4: {}