Algorand MCP
by GoPlausible
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import {
transactionResources,
transactionResourceSchemas,
pendingTransactionInformation,
pendingTransactionsByAddress,
pendingTransactions,
getTransactionParams,
status,
statusAfterBlock,
handleTransactionResources
} from '../../../src/resources/algod/transaction.js';
import { algodClient } from '../../../src/algorand-client.js';
// Mock algosdk client
jest.mock('../../../src/algorand-client.js', () => ({
algodClient: {
pendingTransactionInformation: jest.fn(),
pendingTransactionByAddress: jest.fn(),
pendingTransactionsInformation: jest.fn(),
getTransactionParams: jest.fn(),
status: jest.fn(),
statusAfterBlock: jest.fn()
},
RESOURCE_URIS: {
PENDING_TRANSACTION: 'algorand://transaction/{txid}/pending',
PENDING_TRANSACTIONS_BY_ADDRESS: 'algorand://account/{address}/transactions/pending',
PENDING_TRANSACTIONS: 'algorand://transactions/pending',
TRANSACTION_PARAMS: 'algorand://transaction/params',
NODE_STATUS: 'algorand://node/status',
NODE_STATUS_AFTER_BLOCK: 'algorand://node/status/after/{round}'
}
}));
describe('Algod Transaction Resources', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('Resource Definitions', () => {
it('should define transaction resources', () => {
expect(transactionResources).toHaveLength(6);
expect(transactionResources.map(r => r.name)).toEqual([
'Pending Transaction',
'Pending Transactions By Address',
'All Pending Transactions',
'Transaction Parameters',
'Node Status',
'Node Status After Block'
]);
});
it('should define resource schemas', () => {
expect(Object.keys(transactionResourceSchemas)).toHaveLength(6);
expect(transactionResourceSchemas).toHaveProperty('algorand://transaction/{txid}/pending');
expect(transactionResourceSchemas).toHaveProperty('algorand://account/{address}/transactions/pending');
expect(transactionResourceSchemas).toHaveProperty('algorand://transactions/pending');
expect(transactionResourceSchemas).toHaveProperty('algorand://transaction/params');
expect(transactionResourceSchemas).toHaveProperty('algorand://node/status');
expect(transactionResourceSchemas).toHaveProperty('algorand://node/status/after/{round}');
});
});
describe('Pending Transaction Information', () => {
const mockTxId = 'MOCK_TX_ID';
const mockResponse = {
poolError: '',
txn: { type: 'pay' },
confirmedRound: 1234
};
beforeEach(() => {
(algodClient.pendingTransactionInformation as jest.Mock).mockReturnValue({
do: jest.fn().mockResolvedValue(mockResponse)
});
});
it('should fetch pending transaction information', async () => {
const result = await pendingTransactionInformation(mockTxId);
expect(result).toEqual(mockResponse);
expect(algodClient.pendingTransactionInformation).toHaveBeenCalledWith(mockTxId);
});
it('should handle errors', async () => {
const error = new Error('Network error');
(algodClient.pendingTransactionInformation as jest.Mock).mockReturnValue({
do: jest.fn().mockRejectedValue(error)
});
await expect(pendingTransactionInformation(mockTxId))
.rejects
.toThrow('Failed to get pending transaction: Network error');
});
});
describe('Pending Transactions By Address', () => {
const mockAddress = 'MOCK_ADDRESS';
const mockResponse = {
topTransactions: [{ txn: { type: 'pay' } }],
totalTransactions: 1
};
beforeEach(() => {
(algodClient.pendingTransactionByAddress as jest.Mock).mockReturnValue({
do: jest.fn().mockResolvedValue(mockResponse)
});
});
it('should fetch pending transactions by address', async () => {
const result = await pendingTransactionsByAddress(mockAddress);
expect(result).toEqual(mockResponse);
expect(algodClient.pendingTransactionByAddress).toHaveBeenCalledWith(mockAddress);
});
it('should handle errors', async () => {
const error = new Error('Network error');
(algodClient.pendingTransactionByAddress as jest.Mock).mockReturnValue({
do: jest.fn().mockRejectedValue(error)
});
await expect(pendingTransactionsByAddress(mockAddress))
.rejects
.toThrow('Failed to get pending transactions by address: Network error');
});
});
describe('All Pending Transactions', () => {
const mockResponse = {
topTransactions: [{ txn: { type: 'pay' } }],
totalTransactions: 1
};
beforeEach(() => {
(algodClient.pendingTransactionsInformation as jest.Mock).mockReturnValue({
max: jest.fn().mockReturnThis(),
do: jest.fn().mockResolvedValue(mockResponse)
});
});
it('should fetch all pending transactions', async () => {
const result = await pendingTransactions();
expect(result).toEqual(mockResponse);
expect(algodClient.pendingTransactionsInformation).toHaveBeenCalled();
});
it('should handle max transactions parameter', async () => {
const maxTxns = 10;
await pendingTransactions(maxTxns);
const mockMax = (algodClient.pendingTransactionsInformation as jest.Mock).mock.results[0].value.max;
expect(mockMax).toHaveBeenCalledWith(maxTxns);
});
it('should handle errors', async () => {
const error = new Error('Network error');
(algodClient.pendingTransactionsInformation as jest.Mock).mockReturnValue({
max: jest.fn().mockReturnThis(),
do: jest.fn().mockRejectedValue(error)
});
await expect(pendingTransactions())
.rejects
.toThrow('Failed to get pending transactions: Network error');
});
});
describe('Transaction Parameters', () => {
const mockResponse = {
fee: 1000,
firstRound: 1234,
lastRound: 1235,
genesisHash: 'hash',
genesisID: 'testnet-v1.0'
};
beforeEach(() => {
(algodClient.getTransactionParams as jest.Mock).mockReturnValue({
do: jest.fn().mockResolvedValue(mockResponse)
});
});
it('should fetch transaction parameters', async () => {
const result = await getTransactionParams();
expect(result).toEqual(mockResponse);
expect(algodClient.getTransactionParams).toHaveBeenCalled();
});
it('should handle errors', async () => {
const error = new Error('Network error');
(algodClient.getTransactionParams as jest.Mock).mockReturnValue({
do: jest.fn().mockRejectedValue(error)
});
await expect(getTransactionParams())
.rejects
.toThrow('Failed to get transaction params: Network error');
});
});
describe('Node Status', () => {
const mockResponse = {
lastRound: 1234,
catchupTime: 0,
hasSyncedSinceStartup: true
};
beforeEach(() => {
(algodClient.status as jest.Mock).mockReturnValue({
do: jest.fn().mockResolvedValue(mockResponse)
});
});
it('should fetch node status', async () => {
const result = await status();
expect(result).toEqual(mockResponse);
expect(algodClient.status).toHaveBeenCalled();
});
it('should handle errors', async () => {
const error = new Error('Network error');
(algodClient.status as jest.Mock).mockReturnValue({
do: jest.fn().mockRejectedValue(error)
});
await expect(status())
.rejects
.toThrow('Failed to get status: Network error');
});
});
describe('Status After Block', () => {
const mockRound = 1234;
const mockResponse = {
lastRound: mockRound + 1,
catchupTime: 0,
hasSyncedSinceStartup: true
};
beforeEach(() => {
(algodClient.statusAfterBlock as jest.Mock).mockReturnValue({
do: jest.fn().mockResolvedValue(mockResponse)
});
});
it('should fetch status after block', async () => {
const result = await statusAfterBlock(mockRound);
expect(result).toEqual(mockResponse);
expect(algodClient.statusAfterBlock).toHaveBeenCalledWith(mockRound);
});
it('should handle errors', async () => {
const error = new Error('Network error');
(algodClient.statusAfterBlock as jest.Mock).mockReturnValue({
do: jest.fn().mockRejectedValue(error)
});
await expect(statusAfterBlock(mockRound))
.rejects
.toThrow('Failed to get status after block: Network error');
});
});
describe('Resource Handler', () => {
const mockTxId = 'MOCK_TX_ID';
const mockAddress = 'MOCK_ADDRESS';
const mockRound = 1234;
beforeEach(() => {
// Reset all mocks with success responses
(algodClient.pendingTransactionInformation as jest.Mock).mockReturnValue({
do: jest.fn().mockResolvedValue({ txn: { type: 'pay' }, confirmedRound: mockRound })
});
(algodClient.pendingTransactionByAddress as jest.Mock).mockReturnValue({
do: jest.fn().mockResolvedValue({ topTransactions: [], totalTransactions: 0 })
});
(algodClient.pendingTransactionsInformation as jest.Mock).mockReturnValue({
max: jest.fn().mockReturnThis(),
do: jest.fn().mockResolvedValue({ topTransactions: [], totalTransactions: 0 })
});
(algodClient.getTransactionParams as jest.Mock).mockReturnValue({
do: jest.fn().mockResolvedValue({ fee: 1000 })
});
(algodClient.status as jest.Mock).mockReturnValue({
do: jest.fn().mockResolvedValue({ lastRound: mockRound })
});
(algodClient.statusAfterBlock as jest.Mock).mockReturnValue({
do: jest.fn().mockResolvedValue({ lastRound: mockRound + 1 })
});
});
it('should handle pending transaction URI', async () => {
const uri = `algorand://transaction/${mockTxId}/pending`;
const result = await handleTransactionResources(uri);
expect(result).toHaveLength(1);
expect(JSON.parse(result[0].text)).toHaveProperty('transaction');
expect(JSON.parse(result[0].text)).toHaveProperty('currentRound');
});
it('should handle pending transactions by address URI', async () => {
const uri = `algorand://account/${mockAddress}/transactions/pending`;
const result = await handleTransactionResources(uri);
expect(result).toHaveLength(1);
expect(JSON.parse(result[0].text)).toHaveProperty('transactions');
expect(JSON.parse(result[0].text)).toHaveProperty('totalTransactions');
});
it('should handle all pending transactions URI', async () => {
const uri = 'algorand://transactions/pending';
const result = await handleTransactionResources(uri);
expect(result).toHaveLength(1);
expect(JSON.parse(result[0].text)).toHaveProperty('transactions');
expect(JSON.parse(result[0].text)).toHaveProperty('totalTransactions');
});
it('should handle transaction parameters URI', async () => {
const uri = 'algorand://transaction/params';
const result = await handleTransactionResources(uri);
expect(result).toHaveLength(1);
expect(JSON.parse(result[0].text)).toHaveProperty('params');
});
it('should handle node status URI', async () => {
const uri = 'algorand://node/status';
const result = await handleTransactionResources(uri);
expect(result).toHaveLength(1);
expect(JSON.parse(result[0].text)).toHaveProperty('status');
});
it('should handle status after block URI', async () => {
const uri = `algorand://node/status/after/${mockRound}`;
const result = await handleTransactionResources(uri);
expect(result).toHaveLength(1);
expect(JSON.parse(result[0].text)).toHaveProperty('status');
});
it('should return empty array for unknown URI', async () => {
const uri = 'algorand://unknown';
const result = await handleTransactionResources(uri);
expect(result).toHaveLength(0);
});
it('should handle errors with McpError', async () => {
const error = new Error('Network error');
(algodClient.pendingTransactionInformation as jest.Mock).mockReturnValue({
do: jest.fn().mockRejectedValue(error)
});
const uri = `algorand://transaction/${mockTxId}/pending`;
await expect(handleTransactionResources(uri))
.rejects
.toThrow(new McpError(ErrorCode.InternalError, 'Network error'));
});
});
});