Algorand MCP

import algosdk, { modelsv2 } from 'algosdk'; import { algodClient } from '../../algorand-client.js'; import { URI_TEMPLATES, mcpUriToEndpoint } from '../uri-config.js'; import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { ResourceContent, ResourceDefinition } from '../types.js'; import type { PendingTransactionResponse, NodeStatusResponse, PendingTransactionsResponse } from 'algosdk/dist/types/client/v2/algod/models/types'; import type { SuggestedParamsWithMinFee } from 'algosdk/dist/types/types/transactions/base'; type ResourceSchema = { type: string; properties: { [key: string]: { type: string; description: string; }; }; }; export const transactionResourceSchemas: { [key: string]: ResourceSchema } = { [URI_TEMPLATES.ALGOD_PENDING_TXN]: { type: 'object', properties: { transaction: { type: 'string', description: 'Pending transaction information including sender, receiver, and amount' }, currentRound: { type: 'string', description: 'The round at which this information was current' } } }, [URI_TEMPLATES.ALGOD_PENDING_TXNS_BY_ADDRESS]: { type: 'object', properties: { transactions: { type: 'string', description: 'List of pending transactions for this address' }, totalTransactions: { type: 'string', description: 'Total number of pending transactions' } } }, [URI_TEMPLATES.ALGOD_PENDING_TXNS]: { type: 'object', properties: { transactions: { type: 'string', description: 'List of all pending transactions' }, totalTransactions: { type: 'string', description: 'Total number of pending transactions' } } }, [URI_TEMPLATES.ALGOD_TXN_PARAMS]: { type: 'object', properties: { params: { type: 'string', description: 'Suggested transaction parameters including fee and first/last valid rounds' } } }, [URI_TEMPLATES.ALGOD_STATUS]: { type: 'object', properties: { status: { type: 'string', description: 'Current node status including last round and version' } } }, [URI_TEMPLATES.ALGOD_STATUS_AFTER_BLOCK]: { type: 'object', properties: { status: { type: 'string', description: 'Node status after specified round including last round and version' } } } }; export const transactionResources: ResourceDefinition[] = [ { uri: URI_TEMPLATES.ALGOD_PENDING_TXN, name: 'Pending Transaction', description: 'Get pending transaction information', mimeType: 'application/json', schema: transactionResourceSchemas[URI_TEMPLATES.ALGOD_PENDING_TXN] }, { uri: URI_TEMPLATES.ALGOD_PENDING_TXNS_BY_ADDRESS, name: 'Pending Transactions By Address', description: 'Get pending transactions for an address', mimeType: 'application/json', schema: transactionResourceSchemas[URI_TEMPLATES.ALGOD_PENDING_TXNS_BY_ADDRESS] }, { uri: URI_TEMPLATES.ALGOD_PENDING_TXNS, name: 'All Pending Transactions', description: 'Get all pending transactions', mimeType: 'application/json', schema: transactionResourceSchemas[URI_TEMPLATES.ALGOD_PENDING_TXNS] }, { uri: URI_TEMPLATES.ALGOD_TXN_PARAMS, name: 'Transaction Parameters', description: 'Get suggested transaction parameters', mimeType: 'application/json', schema: transactionResourceSchemas[URI_TEMPLATES.ALGOD_TXN_PARAMS] }, { uri: URI_TEMPLATES.ALGOD_STATUS, name: 'Node Status', description: 'Get current node status', mimeType: 'application/json', schema: transactionResourceSchemas[URI_TEMPLATES.ALGOD_STATUS] }, { uri: URI_TEMPLATES.ALGOD_STATUS_AFTER_BLOCK, name: 'Node Status After Block', description: 'Get node status after a specific round', mimeType: 'application/json', schema: transactionResourceSchemas[URI_TEMPLATES.ALGOD_STATUS_AFTER_BLOCK] } ]; export async function handleTransactionResources(uri: string): Promise<ResourceContent[]> { try { // Validate URI format if (!uri.startsWith('algorand://')) { return []; } let match; // Pending transaction match = uri.match(/^algorand:\/\/algod\/transactions\/pending\/([^/]+)$/); if (match) { const txId = match[1]; const info = await pendingTransactionInformation(txId); return [{ uri, mimeType: 'application/json', text: JSON.stringify({ transaction: info, currentRound: info.poolError ? 0 : info.confirmedRound }, null, 2), }]; } // Pending transactions by address match = uri.match(/^algorand:\/\/algod\/accounts\/([^/]+)\/transactions\/pending$/); if (match) { const address = match[1]; const info = await pendingTransactionsByAddress(address); return [{ uri, mimeType: 'application/json', text: JSON.stringify({ transactions: info.topTransactions, totalTransactions: info.totalTransactions }, null, 2), }]; } // All pending transactions match = uri.match(/^algorand:\/\/algod\/transactions\/pending$/); if (match) { const info = await pendingTransactions(); return [{ uri, mimeType: 'application/json', text: JSON.stringify({ transactions: info.topTransactions, totalTransactions: info.totalTransactions }, null, 2), }]; } // Transaction parameters match = uri.match(/^algorand:\/\/algod\/transactions\/params$/); if (match) { const params = await getTransactionParams(); return [{ uri, mimeType: 'application/json', text: JSON.stringify({ params }, null, 2), }]; } // Node status match = uri.match(/^algorand:\/\/algod\/status$/); if (match) { const nodeStatus = await status(); return [{ uri, mimeType: 'application/json', text: JSON.stringify({ status: nodeStatus }, null, 2), }]; } // Node status after block match = uri.match(/^algorand:\/\/algod\/status\/wait-for-block-after\/([^/]+)$/); if (match) { const round = parseInt(match[1], 10); const nodeStatus = await statusAfterBlock(round); return [{ uri, mimeType: 'application/json', text: JSON.stringify({ status: nodeStatus }, null, 2), }]; } return []; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to handle resource: ${error instanceof Error ? error.message : String(error)}` ); } } export async function pendingTransactionInformation(txId: string): Promise<PendingTransactionResponse> { try { const response = await algodClient.pendingTransactionInformation(txId).do() as PendingTransactionResponse; return response; } catch (error) { throw new Error(`Failed to get pending transaction: ${error instanceof Error ? error.message : String(error)}`); } } export async function pendingTransactionsByAddress(address: string): Promise<PendingTransactionsResponse> { try { const response = await algodClient.pendingTransactionByAddress(address).do() as PendingTransactionsResponse; return response; } catch (error) { throw new Error(`Failed to get pending transactions by address: ${error instanceof Error ? error.message : String(error)}`); } } export async function pendingTransactions(maxTxns?: number): Promise<PendingTransactionsResponse> { try { let search = algodClient.pendingTransactionsInformation(); if (maxTxns !== undefined) { search = search.max(maxTxns); } return await search.do() as PendingTransactionsResponse; } catch (error) { throw new Error(`Failed to get pending transactions: ${error instanceof Error ? error.message : String(error)}`); } } export async function getTransactionParams(): Promise<SuggestedParamsWithMinFee> { try { const response = await algodClient.getTransactionParams().do(); return response; } catch (error) { throw new Error(`Failed to get transaction params: ${error instanceof Error ? error.message : String(error)}`); } } export async function status(): Promise<NodeStatusResponse> { try { const response = await algodClient.status().do() as NodeStatusResponse; return response; } catch (error) { throw new Error(`Failed to get status: ${error instanceof Error ? error.message : String(error)}`); } } export async function statusAfterBlock(round: number): Promise<NodeStatusResponse> { try { const response = await algodClient.statusAfterBlock(round).do() as NodeStatusResponse; return response; } catch (error) { throw new Error(`Failed to get status after block: ${error instanceof Error ? error.message : String(error)}`); } }