Algorand MCP

import { Transaction, makeAssetCreateTxnWithSuggestedParamsFromObject, makeAssetConfigTxnWithSuggestedParamsFromObject, makeAssetDestroyTxnWithSuggestedParamsFromObject, makeAssetFreezeTxnWithSuggestedParamsFromObject, makeAssetTransferTxnWithSuggestedParamsFromObject, SuggestedParams } from 'algosdk'; import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { algodClient } from '../../algorand-client.js'; // Tool schemas export const assetTransactionSchemas = { makeAssetCreateTxn: { type: 'object', properties: { from: { type: 'string' }, total: { type: 'integer' }, decimals: { type: 'integer' }, defaultFrozen: { type: 'boolean' }, unitName: { type: 'string', optional: true }, assetName: { type: 'string', optional: true }, assetURL: { type: 'string', optional: true }, assetMetadataHash: { type: 'string', optional: true }, manager: { type: 'string', optional: true }, reserve: { type: 'string', optional: true }, freeze: { type: 'string', optional: true }, clawback: { type: 'string', optional: true }, note: { type: 'string', optional: true }, rekeyTo: { type: 'string', optional: true } }, required: ['from', 'total', 'decimals', 'defaultFrozen'] }, makeAssetConfigTxn: { type: 'object', properties: { from: { type: 'string' }, assetIndex: { type: 'integer' }, manager: { type: 'string', optional: true }, reserve: { type: 'string', optional: true }, freeze: { type: 'string', optional: true }, clawback: { type: 'string', optional: true }, strictEmptyAddressChecking: { type: 'boolean' }, note: { type: 'string', optional: true }, rekeyTo: { type: 'string', optional: true } }, required: ['from', 'assetIndex', 'strictEmptyAddressChecking'] }, makeAssetDestroyTxn: { type: 'object', properties: { from: { type: 'string' }, assetIndex: { type: 'integer' }, note: { type: 'string', optional: true }, rekeyTo: { type: 'string', optional: true } }, required: ['from', 'assetIndex'] }, makeAssetFreezeTxn: { type: 'object', properties: { from: { type: 'string' }, assetIndex: { type: 'integer' }, freezeTarget: { type: 'string' }, freezeState: { type: 'boolean' }, note: { type: 'string', optional: true }, rekeyTo: { type: 'string', optional: true } }, required: ['from', 'assetIndex', 'freezeTarget', 'freezeState'] }, makeAssetTransferTxn: { type: 'object', properties: { from: { type: 'string' }, to: { type: 'string' }, assetIndex: { type: 'integer' }, amount: { type: 'integer' }, note: { type: 'string', optional: true }, closeRemainderTo: { type: 'string', optional: true }, rekeyTo: { type: 'string', optional: true } }, required: ['from', 'to', 'assetIndex', 'amount'] } }; // Tool definitions export const assetTransactionTools = [ { name: 'make_asset_create_txn', description: 'Create an asset creation transaction', inputSchema: assetTransactionSchemas.makeAssetCreateTxn, }, { name: 'make_asset_config_txn', description: 'Create an asset configuration transaction', inputSchema: assetTransactionSchemas.makeAssetConfigTxn, }, { name: 'make_asset_destroy_txn', description: 'Create an asset destroy transaction', inputSchema: assetTransactionSchemas.makeAssetDestroyTxn, }, { name: 'make_asset_freeze_txn', description: 'Create an asset freeze transaction', inputSchema: assetTransactionSchemas.makeAssetFreezeTxn, }, { name: 'make_asset_transfer_txn', description: 'Create an asset transfer transaction', inputSchema: assetTransactionSchemas.makeAssetTransferTxn, } ]; export class AssetTransactionManager { /** * Creates an asset creation transaction */ static makeAssetCreateTxn(params: { from: string; note?: Uint8Array; rekeyTo?: string; suggestedParams: SuggestedParams; total: number | bigint; decimals: number; defaultFrozen: boolean; unitName?: string; assetName?: string; assetURL?: string; assetMetadataHash?: string | Uint8Array; manager?: string; reserve?: string; freeze?: string; clawback?: string; }): Transaction { return makeAssetCreateTxnWithSuggestedParamsFromObject(params); } /** * Creates an asset configuration transaction */ static makeAssetConfigTxn(params: { from: string; note?: Uint8Array; rekeyTo?: string; suggestedParams: SuggestedParams; assetIndex: number; manager?: string; reserve?: string; freeze?: string; clawback?: string; strictEmptyAddressChecking: boolean; }): Transaction { return makeAssetConfigTxnWithSuggestedParamsFromObject(params); } /** * Creates an asset destroy transaction */ static makeAssetDestroyTxn(params: { from: string; note?: Uint8Array; rekeyTo?: string; suggestedParams: SuggestedParams; assetIndex: number; }): Transaction { return makeAssetDestroyTxnWithSuggestedParamsFromObject(params); } /** * Creates an asset freeze transaction */ static makeAssetFreezeTxn(params: { from: string; note?: Uint8Array; rekeyTo?: string; suggestedParams: SuggestedParams; assetIndex: number; freezeTarget: string; freezeState: boolean; }): Transaction { return makeAssetFreezeTxnWithSuggestedParamsFromObject(params); } /** * Creates an asset transfer transaction */ static makeAssetTransferTxn(params: { from: string; to: string; closeRemainderTo?: string; revocationTarget?: string; amount: number | bigint; note?: Uint8Array; rekeyTo?: string; suggestedParams: SuggestedParams; assetIndex: number; }): Transaction { return makeAssetTransferTxnWithSuggestedParamsFromObject(params); } // Tool handlers static async handleTool(name: string, args: Record<string, unknown>) { const params = await algodClient.getTransactionParams().do(); const suggestedParams = { ...params, flatFee: true, fee: params.minFee }; switch (name) { case 'make_asset_create_txn': if (!args.from || typeof args.total !== 'number' || typeof args.decimals !== 'number' || typeof args.defaultFrozen !== 'boolean') { throw new McpError(ErrorCode.InvalidParams, 'Invalid asset creation parameters'); } // Create transaction with proper parameter handling const createTxnParams: Record<string, any> = { from: String(args.from), total: Number(args.total), decimals: Number(args.decimals), defaultFrozen: Boolean(args.defaultFrozen), fee: suggestedParams.fee, firstRound: suggestedParams.firstRound, lastRound: suggestedParams.lastRound, genesisID: suggestedParams.genesisID, genesisHash: suggestedParams.genesisHash, type: 'acfg' }; // Handle optional fields if (typeof args.unitName === 'string') { createTxnParams.unitName = args.unitName; } if (typeof args.assetName === 'string') { createTxnParams.assetName = args.assetName; } if (typeof args.assetURL === 'string') { createTxnParams.assetURL = args.assetURL; } if (typeof args.assetMetadataHash === 'string') { const metadataBytes = new TextEncoder().encode(args.assetMetadataHash); createTxnParams.assetMetadataHash = Buffer.from(metadataBytes).toString('base64'); } if (typeof args.manager === 'string') { createTxnParams.manager = args.manager; } if (typeof args.reserve === 'string') { createTxnParams.reserve = args.reserve; } if (typeof args.freeze === 'string') { createTxnParams.freeze = args.freeze; } if (typeof args.clawback === 'string') { createTxnParams.clawback = args.clawback; } if (typeof args.note === 'string') { const noteBytes = new TextEncoder().encode(args.note); createTxnParams.note = Buffer.from(noteBytes).toString('base64'); } if (typeof args.rekeyTo === 'string') { createTxnParams.rekeyTo = args.rekeyTo; } return { content: [{ type: 'text', text: JSON.stringify(createTxnParams, null, 2), }], }; case 'make_asset_config_txn': if (!args.from || typeof args.assetIndex !== 'number' || typeof args.strictEmptyAddressChecking !== 'boolean') { throw new McpError(ErrorCode.InvalidParams, 'Invalid asset configuration parameters'); } // Create transaction with proper parameter handling const configTxnParams: Record<string, any> = { from: String(args.from), assetIndex: Number(args.assetIndex), fee: suggestedParams.fee, firstRound: suggestedParams.firstRound, lastRound: suggestedParams.lastRound, genesisID: suggestedParams.genesisID, genesisHash: suggestedParams.genesisHash, type: 'acfg', strictEmptyAddressChecking: Boolean(args.strictEmptyAddressChecking) }; // Handle optional fields if (typeof args.manager === 'string') { configTxnParams.manager = args.manager; } if (typeof args.reserve === 'string') { configTxnParams.reserve = args.reserve; } if (typeof args.freeze === 'string') { configTxnParams.freeze = args.freeze; } if (typeof args.clawback === 'string') { configTxnParams.clawback = args.clawback; } if (typeof args.note === 'string') { const noteBytes = new TextEncoder().encode(args.note); configTxnParams.note = Buffer.from(noteBytes).toString('base64'); } if (typeof args.rekeyTo === 'string') { configTxnParams.rekeyTo = args.rekeyTo; } return { content: [{ type: 'text', text: JSON.stringify(configTxnParams, null, 2), }], }; case 'make_asset_destroy_txn': if (!args.from || typeof args.assetIndex !== 'number') { throw new McpError(ErrorCode.InvalidParams, 'Invalid asset destroy parameters'); } // Create transaction with proper parameter handling const destroyTxnParams: Record<string, any> = { from: String(args.from), assetIndex: Number(args.assetIndex), fee: suggestedParams.fee, firstRound: suggestedParams.firstRound, lastRound: suggestedParams.lastRound, genesisID: suggestedParams.genesisID, genesisHash: suggestedParams.genesisHash, type: 'acfg' }; // Handle optional fields if (typeof args.note === 'string') { const noteBytes = new TextEncoder().encode(args.note); destroyTxnParams.note = Buffer.from(noteBytes).toString('base64'); } if (typeof args.rekeyTo === 'string') { destroyTxnParams.rekeyTo = args.rekeyTo; } return { content: [{ type: 'text', text: JSON.stringify(destroyTxnParams, null, 2), }], }; case 'make_asset_freeze_txn': if (!args.from || typeof args.assetIndex !== 'number' || !args.freezeTarget || typeof args.freezeState !== 'boolean') { throw new McpError(ErrorCode.InvalidParams, 'Invalid asset freeze parameters'); } // Create transaction with proper parameter handling const freezeTxnParams: Record<string, any> = { from: String(args.from), assetIndex: Number(args.assetIndex), freezeTarget: String(args.freezeTarget), freezeState: Boolean(args.freezeState), fee: suggestedParams.fee, firstRound: suggestedParams.firstRound, lastRound: suggestedParams.lastRound, genesisID: suggestedParams.genesisID, genesisHash: suggestedParams.genesisHash, type: 'afrz' }; // Handle optional fields if (typeof args.note === 'string') { const noteBytes = new TextEncoder().encode(args.note); freezeTxnParams.note = Buffer.from(noteBytes).toString('base64'); } if (typeof args.rekeyTo === 'string') { freezeTxnParams.rekeyTo = args.rekeyTo; } return { content: [{ type: 'text', text: JSON.stringify(freezeTxnParams, null, 2), }], }; case 'make_asset_transfer_txn': if (!args.from || !args.to || !args.assetIndex || typeof args.amount !== 'number') { throw new McpError(ErrorCode.InvalidParams, 'Invalid asset transfer parameters'); } // Create transaction with proper parameter handling const transferTxnParams: Record<string, any> = { from: String(args.from), to: String(args.to), assetIndex: Number(args.assetIndex), amount: Number(args.amount), fee: suggestedParams.fee, firstRound: suggestedParams.firstRound, lastRound: suggestedParams.lastRound, genesisID: suggestedParams.genesisID, genesisHash: suggestedParams.genesisHash, type: 'axfer' }; // Handle optional fields if (typeof args.note === 'string') { const noteBytes = new TextEncoder().encode(args.note); transferTxnParams.note = Buffer.from(noteBytes).toString('base64'); } if (typeof args.closeRemainderTo === 'string') { transferTxnParams.closeRemainderTo = args.closeRemainderTo; } if (typeof args.rekeyTo === 'string') { transferTxnParams.rekeyTo = args.rekeyTo; } return { content: [{ type: 'text', text: JSON.stringify(transferTxnParams, null, 2), }], }; default: throw new McpError( ErrorCode.MethodNotFound, `Unknown asset transaction tool: ${name}` ); } } }