Algorand MCP
by GoPlausible
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import {
accountResources,
accountResourceSchemas,
lookupAccountByID,
lookupAccountTransactions,
lookupAccountAssets,
searchAccounts,
handleAccountResources
} from '../../../src/resources/indexer/account.js';
import { indexerClient } from '../../../src/algorand-client.js';
// Mock algosdk client
jest.mock('../../../src/algorand-client.js', () => ({
indexerClient: {
lookupAccountByID: jest.fn(),
lookupAccountTransactions: jest.fn(),
lookupAccountAssets: jest.fn(),
searchAccounts: jest.fn()
},
RESOURCE_URIS: {
TRANSACTION_HISTORY: 'algorand://account/{address}/transactions',
ASSET_HOLDINGS: 'algorand://account/{address}/assets'
}
}));
describe('Indexer Account Resources', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('Resource Definitions', () => {
it('should define account resources', () => {
expect(accountResources).toHaveLength(2);
expect(accountResources.map(r => r.name)).toEqual([
'Transaction History',
'Asset Holdings'
]);
});
it('should define resource schemas', () => {
expect(Object.keys(accountResourceSchemas)).toHaveLength(2);
expect(accountResourceSchemas).toHaveProperty('algorand://account/{address}/transactions');
expect(accountResourceSchemas).toHaveProperty('algorand://account/{address}/assets');
});
});
describe('Account Information', () => {
const mockAddress = 'MOCK_ADDRESS';
const mockResponse = {
account: {
address: mockAddress,
amount: 1000000,
assets: []
},
currentRound: 1234
};
beforeEach(() => {
(indexerClient.lookupAccountByID as jest.Mock).mockReturnValue({
do: jest.fn().mockResolvedValue(mockResponse)
});
});
it('should fetch account information', async () => {
const result = await lookupAccountByID(mockAddress);
expect(result).toEqual(mockResponse);
expect(indexerClient.lookupAccountByID).toHaveBeenCalledWith(mockAddress);
});
it('should handle errors', async () => {
const error = new Error('Network error');
(indexerClient.lookupAccountByID as jest.Mock).mockReturnValue({
do: jest.fn().mockRejectedValue(error)
});
await expect(lookupAccountByID(mockAddress))
.rejects
.toThrow('Failed to get account info: Network error');
});
});
describe('Account Transactions', () => {
const mockAddress = 'MOCK_ADDRESS';
const mockResponse = {
transactions: [{ id: 'txn1' }],
currentRound: 1234
};
beforeEach(() => {
(indexerClient.lookupAccountTransactions as jest.Mock).mockReturnValue({
limit: jest.fn().mockReturnThis(),
beforeTime: jest.fn().mockReturnThis(),
afterTime: jest.fn().mockReturnThis(),
nextToken: jest.fn().mockReturnThis(),
do: jest.fn().mockResolvedValue(mockResponse)
});
});
it('should fetch account transactions', async () => {
const result = await lookupAccountTransactions(mockAddress);
expect(result).toEqual(mockResponse);
expect(indexerClient.lookupAccountTransactions).toHaveBeenCalledWith(mockAddress);
});
it('should handle search parameters', async () => {
const params = {
limit: 10,
beforeTime: '2023-01-01',
afterTime: '2022-01-01',
nextToken: 'token123'
};
await lookupAccountTransactions(mockAddress, params);
const mock = indexerClient.lookupAccountTransactions as jest.Mock;
const chain = mock.mock.results[0].value;
expect(chain.limit).toHaveBeenCalledWith(params.limit);
expect(chain.beforeTime).toHaveBeenCalledWith(params.beforeTime);
expect(chain.afterTime).toHaveBeenCalledWith(params.afterTime);
expect(chain.nextToken).toHaveBeenCalledWith(params.nextToken);
});
it('should handle errors', async () => {
const error = new Error('Network error');
(indexerClient.lookupAccountTransactions as jest.Mock).mockReturnValue({
do: jest.fn().mockRejectedValue(error)
});
await expect(lookupAccountTransactions(mockAddress))
.rejects
.toThrow('Failed to get account transactions: Network error');
});
});
describe('Account Assets', () => {
const mockAddress = 'MOCK_ADDRESS';
const mockResponse = {
assets: [{ assetId: 1, amount: 100 }],
currentRound: 1234
};
beforeEach(() => {
(indexerClient.lookupAccountAssets as jest.Mock).mockReturnValue({
limit: jest.fn().mockReturnThis(),
assetId: jest.fn().mockReturnThis(),
nextToken: jest.fn().mockReturnThis(),
do: jest.fn().mockResolvedValue(mockResponse)
});
});
it('should fetch account assets', async () => {
const result = await lookupAccountAssets(mockAddress);
expect(result).toEqual(mockResponse);
expect(indexerClient.lookupAccountAssets).toHaveBeenCalledWith(mockAddress);
});
it('should handle search parameters', async () => {
const params = {
limit: 10,
assetId: 123,
nextToken: 'token123'
};
await lookupAccountAssets(mockAddress, params);
const mock = indexerClient.lookupAccountAssets as jest.Mock;
const chain = mock.mock.results[0].value;
expect(chain.limit).toHaveBeenCalledWith(params.limit);
expect(chain.assetId).toHaveBeenCalledWith(params.assetId);
expect(chain.nextToken).toHaveBeenCalledWith(params.nextToken);
});
it('should handle errors', async () => {
const error = new Error('Network error');
(indexerClient.lookupAccountAssets as jest.Mock).mockReturnValue({
do: jest.fn().mockRejectedValue(error)
});
await expect(lookupAccountAssets(mockAddress))
.rejects
.toThrow('Failed to get account assets: Network error');
});
});
describe('Search Accounts', () => {
const mockResponse = {
accounts: [{ address: 'addr1' }],
currentRound: 1234
};
beforeEach(() => {
(indexerClient.searchAccounts as jest.Mock).mockReturnValue({
limit: jest.fn().mockReturnThis(),
assetID: jest.fn().mockReturnThis(),
applicationID: jest.fn().mockReturnThis(),
currencyGreaterThan: jest.fn().mockReturnThis(),
currencyLessThan: jest.fn().mockReturnThis(),
nextToken: jest.fn().mockReturnThis(),
do: jest.fn().mockResolvedValue(mockResponse)
});
});
it('should search accounts', async () => {
const result = await searchAccounts();
expect(result).toEqual(mockResponse);
expect(indexerClient.searchAccounts).toHaveBeenCalled();
});
it('should handle search parameters', async () => {
const params = {
limit: 10,
assetId: 123,
applicationId: 456,
currencyGreaterThan: 1000,
currencyLessThan: 2000,
nextToken: 'token123'
};
await searchAccounts(params);
const mock = indexerClient.searchAccounts as jest.Mock;
const chain = mock.mock.results[0].value;
expect(chain.limit).toHaveBeenCalledWith(params.limit);
expect(chain.assetID).toHaveBeenCalledWith(params.assetId);
expect(chain.applicationID).toHaveBeenCalledWith(params.applicationId);
expect(chain.currencyGreaterThan).toHaveBeenCalledWith(params.currencyGreaterThan);
expect(chain.currencyLessThan).toHaveBeenCalledWith(params.currencyLessThan);
expect(chain.nextToken).toHaveBeenCalledWith(params.nextToken);
});
it('should handle errors', async () => {
const error = new Error('Network error');
(indexerClient.searchAccounts as jest.Mock).mockReturnValue({
do: jest.fn().mockRejectedValue(error)
});
await expect(searchAccounts())
.rejects
.toThrow('Failed to search accounts: Network error');
});
});
describe('Resource Handler', () => {
const mockAddress = 'MOCK_ADDRESS';
beforeEach(() => {
(indexerClient.lookupAccountTransactions as jest.Mock).mockReturnValue({
do: jest.fn().mockResolvedValue({
transactions: [],
currentRound: 1234
})
});
(indexerClient.lookupAccountAssets as jest.Mock).mockReturnValue({
do: jest.fn().mockResolvedValue({
assets: [],
currentRound: 1234
})
});
});
it('should handle transaction history URI', async () => {
const uri = `algorand://account/${mockAddress}/transactions`;
const result = await handleAccountResources(uri);
expect(result).toHaveLength(1);
expect(JSON.parse(result[0].text)).toHaveProperty('transactions');
expect(JSON.parse(result[0].text)).toHaveProperty('currentRound');
});
it('should handle asset holdings URI', async () => {
const uri = `algorand://account/${mockAddress}/assets`;
const result = await handleAccountResources(uri);
expect(result).toHaveLength(1);
expect(JSON.parse(result[0].text)).toHaveProperty('assets');
expect(JSON.parse(result[0].text)).toHaveProperty('currentRound');
});
it('should return empty array for unknown URI', async () => {
const uri = 'algorand://unknown';
const result = await handleAccountResources(uri);
expect(result).toHaveLength(0);
});
it('should handle errors with McpError', async () => {
const error = new Error('Network error');
(indexerClient.lookupAccountTransactions as jest.Mock).mockReturnValue({
do: jest.fn().mockRejectedValue(error)
});
const uri = `algorand://account/${mockAddress}/transactions`;
await expect(handleAccountResources(uri))
.rejects
.toThrow(new McpError(ErrorCode.InternalError, 'Failed to get transaction history: Network error'));
});
});
});