Algorand MCP
by GoPlausible
import algosdk from 'algosdk';
import { indexerClient } 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 {
Asset,
AssetResponse,
AssetsResponse,
AssetBalancesResponse,
MiniAssetHolding,
TransactionsResponse
} from 'algosdk/dist/types/client/v2/indexer/models/types';
type ResourceSchema = {
type: string;
properties: {
[key: string]: {
type: string;
description: string;
};
};
};
export const assetResourceSchemas: { [key: string]: ResourceSchema } = {
[URI_TEMPLATES.INDEXER_ASSETS]: {
type: 'object',
properties: {
assets: {
type: 'string',
description: 'List of assets matching the search criteria with their configurations'
},
currentRound: {
type: 'string',
description: 'The round at which this information was current'
}
}
},
[URI_TEMPLATES.INDEXER_ASSET]: {
type: 'object',
properties: {
asset: {
type: 'string',
description: 'Asset information including total supply, decimals, and creator'
},
currentRound: {
type: 'string',
description: 'The round at which this information was current'
}
}
},
[URI_TEMPLATES.INDEXER_ASSET_BALANCES]: {
type: 'object',
properties: {
balances: {
type: 'string',
description: 'List of accounts holding this asset with their balances'
},
currentRound: {
type: 'string',
description: 'The round at which this information was current'
}
}
},
[URI_TEMPLATES.INDEXER_ASSET_TRANSACTIONS]: {
type: 'object',
properties: {
transactions: {
type: 'string',
description: 'List of transactions involving this asset with details'
},
currentRound: {
type: 'string',
description: 'The round at which this information was current'
}
}
},
[URI_TEMPLATES.INDEXER_ASSET_BALANCE_BY_ADDRESS]: {
type: 'object',
properties: {
balance: {
type: 'string',
description: 'Asset balance and opt-in status for the specified account'
},
currentRound: {
type: 'string',
description: 'The round at which this information was current'
}
}
},
[URI_TEMPLATES.INDEXER_ASSET_TRANSACTION_BY_ID]: {
type: 'object',
properties: {
transaction: {
type: 'string',
description: 'Detailed transaction information including sender, receiver, and amount'
},
currentRound: {
type: 'string',
description: 'The round at which this information was current'
}
}
}
};
export const assetResources: ResourceDefinition[] = [
{
uri: URI_TEMPLATES.INDEXER_ASSET,
name: 'Asset Details',
description: 'Get asset information and configuration',
mimeType: 'application/json',
schema: assetResourceSchemas[URI_TEMPLATES.INDEXER_ASSET]
},
{
uri: URI_TEMPLATES.INDEXER_ASSET_BALANCES,
name: 'Asset Balances',
description: 'Get accounts holding this asset and their balances',
mimeType: 'application/json',
schema: assetResourceSchemas[URI_TEMPLATES.INDEXER_ASSET_BALANCES]
},
{
uri: URI_TEMPLATES.INDEXER_ASSET_TRANSACTIONS,
name: 'Asset Transactions',
description: 'Get transactions involving this asset',
mimeType: 'application/json',
schema: assetResourceSchemas[URI_TEMPLATES.INDEXER_ASSET_TRANSACTIONS]
},
{
uri: URI_TEMPLATES.INDEXER_ASSET_BALANCE_BY_ADDRESS,
name: 'Asset Balance By Address',
description: 'Get specific account balance for this asset',
mimeType: 'application/json',
schema: assetResourceSchemas[URI_TEMPLATES.INDEXER_ASSET_BALANCE_BY_ADDRESS]
},
{
uri: URI_TEMPLATES.INDEXER_ASSET_TRANSACTION_BY_ID,
name: 'Asset Transaction By ID',
description: 'Get specific transaction details for this asset',
mimeType: 'application/json',
schema: assetResourceSchemas[URI_TEMPLATES.INDEXER_ASSET_TRANSACTION_BY_ID]
},
{
uri: URI_TEMPLATES.INDEXER_ASSETS,
name: 'Search Assets',
description: 'Search for assets with various criteria',
mimeType: 'application/json',
schema: assetResourceSchemas[URI_TEMPLATES.INDEXER_ASSETS]
}
];
export async function lookupAssetByID(assetId: number): Promise<AssetResponse> {
try {
const response = await indexerClient.lookupAssetByID(assetId).do() as AssetResponse;
return response;
} catch (error) {
throw new Error(`Failed to get asset info: ${error instanceof Error ? error.message : String(error)}`);
}
}
export async function lookupAssetBalances(assetId: number, params?: {
limit?: number;
currencyGreaterThan?: number;
currencyLessThan?: number;
nextToken?: string;
address?: string;
}): Promise<AssetBalancesResponse> {
try {
let search = indexerClient.lookupAssetBalances(assetId);
if (params?.limit) {
search = search.limit(params.limit);
}
if (params?.currencyGreaterThan) {
search = search.currencyGreaterThan(params.currencyGreaterThan);
}
if (params?.currencyLessThan) {
search = search.currencyLessThan(params.currencyLessThan);
}
if (params?.nextToken) {
search = search.nextToken(params.nextToken);
}
return await search.do() as AssetBalancesResponse;
} catch (error) {
throw new Error(`Failed to get asset balances: ${error instanceof Error ? error.message : String(error)}`);
}
}
export async function lookupAssetTransactions(assetId: number, params?: {
limit?: number;
beforeTime?: string;
afterTime?: string;
minRound?: number;
maxRound?: number;
address?: string;
addressRole?: string;
excludeCloseTo?: boolean;
nextToken?: string;
txid?: string;
}): Promise<TransactionsResponse> {
try {
let search = indexerClient.lookupAssetTransactions(assetId);
if (params?.limit) {
search = search.limit(params.limit);
}
if (params?.beforeTime) {
search = search.beforeTime(params.beforeTime);
}
if (params?.afterTime) {
search = search.afterTime(params.afterTime);
}
if (params?.minRound) {
search = search.minRound(params.minRound);
}
if (params?.maxRound) {
search = search.maxRound(params.maxRound);
}
if (params?.address) {
search = search.address(params.address);
}
if (params?.addressRole) {
search = search.addressRole(params.addressRole);
}
if (params?.excludeCloseTo) {
search = search.excludeCloseTo(params.excludeCloseTo);
}
if (params?.nextToken) {
search = search.nextToken(params.nextToken);
}
return await search.do() as TransactionsResponse;
} catch (error) {
throw new Error(`Failed to get asset transactions: ${error instanceof Error ? error.message : String(error)}`);
}
}
export async function handleAssetResources(uri: string): Promise<ResourceContent[]> {
try {
// Validate URI format
if (!uri.startsWith('algorand://')) {
return [];
}
let match;
// Asset details
match = uri.match(/^algorand:\/\/indexer\/assets\/([^/]+)$/);
if (match) {
const assetId = parseInt(match[1], 10);
const details = await lookupAssetByID(assetId);
return [{
uri,
mimeType: 'application/json',
text: JSON.stringify({
asset: details.asset,
currentRound: details.currentRound
}, null, 2),
}];
}
// Asset balances
match = uri.match(/^algorand:\/\/indexer\/assets\/([^/]+)\/balances$/);
if (match) {
const assetId = parseInt(match[1], 10);
const balances = await lookupAssetBalances(assetId);
return [{
uri,
mimeType: 'application/json',
text: JSON.stringify({
balances: balances.balances,
currentRound: balances.currentRound
}, null, 2),
}];
}
// Asset transactions
match = uri.match(/^algorand:\/\/indexer\/assets\/([^/]+)\/transactions$/);
if (match) {
const assetId = parseInt(match[1], 10);
const transactions = await lookupAssetTransactions(assetId);
return [{
uri,
mimeType: 'application/json',
text: JSON.stringify({
transactions: transactions.transactions,
currentRound: transactions.currentRound
}, null, 2),
}];
}
// Asset balance by address
match = uri.match(/^algorand:\/\/indexer\/assets\/([^/]+)\/balances\/([^/]+)$/);
if (match) {
const [, assetId, address] = match;
const balances = await lookupAssetBalances(parseInt(assetId, 10), { address });
return [{
uri,
mimeType: 'application/json',
text: JSON.stringify({
balance: balances.balances[0],
currentRound: balances.currentRound
}, null, 2),
}];
}
// Asset transaction by ID
match = uri.match(/^algorand:\/\/indexer\/assets\/([^/]+)\/transactions\/([^/]+)$/);
if (match) {
const [, assetId, txid] = match;
const transactions = await lookupAssetTransactions(parseInt(assetId, 10), { txid });
return [{
uri,
mimeType: 'application/json',
text: JSON.stringify({
transaction: transactions.transactions[0],
currentRound: transactions.currentRound
}, null, 2),
}];
}
// Search assets
match = uri.match(/^algorand:\/\/indexer\/assets$/);
if (match) {
const assets = await searchForAssets();
return [{
uri,
mimeType: 'application/json',
text: JSON.stringify({
assets: assets.assets,
currentRound: assets.currentRound
}, null, 2),
}];
}
return [];
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to handle resource: ${error instanceof Error ? error.message : String(error)}`
);
}
}
export async function searchForAssets(params?: {
limit?: number;
creator?: string;
name?: string;
unit?: string;
assetId?: number;
nextToken?: string;
}): Promise<AssetsResponse> {
try {
let search = indexerClient.searchForAssets();
if (params?.limit) {
search = search.limit(params.limit);
}
if (params?.creator) {
search = search.creator(params.creator);
}
if (params?.name) {
search = search.name(params.name);
}
if (params?.unit) {
search = search.unit(params.unit);
}
if (params?.assetId) {
search = search.index(params.assetId);
}
if (params?.nextToken) {
search = search.nextToken(params.nextToken);
}
return await search.do() as AssetsResponse;
} catch (error) {
throw new Error(`Failed to search assets: ${error instanceof Error ? error.message : String(error)}`);
}
}