Algorand MCP

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 { Account, AccountResponse, AccountsResponse, ApplicationLocalStatesResponse, AssetHoldingsResponse, AssetsResponse, ApplicationsResponse, TransactionsResponse } from 'algosdk/dist/types/client/v2/indexer/models/types'; type ResourceSchema = { type: string; properties: { [key: string]: { type: string; description: string; }; }; }; export const accountResourceSchemas: { [key: string]: ResourceSchema } = { [URI_TEMPLATES.INDEXER_ACCOUNT]: { type: 'object', properties: { account: { type: 'string', description: 'Account information including balance, assets, and applications' }, currentRound: { type: 'string', description: 'The round at which this information was current' } } }, [URI_TEMPLATES.INDEXER_ACCOUNT_TRANSACTIONS]: { type: 'object', properties: { transactions: { type: 'string', description: 'List of transactions for this account with details' }, currentRound: { type: 'string', description: 'The round at which this information was current' } } }, [URI_TEMPLATES.INDEXER_ACCOUNT_APPS_LOCAL_STATE]: { type: 'object', properties: { appsLocalStates: { type: 'string', description: 'List of applications this account has opted into with local state' }, currentRound: { type: 'string', description: 'The round at which this information was current' } } }, [URI_TEMPLATES.INDEXER_ACCOUNT_CREATED_APPS]: { type: 'object', properties: { applications: { type: 'string', description: 'List of applications created by this account' }, currentRound: { type: 'string', description: 'The round at which this information was current' } } } }; export const accountResources: ResourceDefinition[] = [ { uri: URI_TEMPLATES.INDEXER_ACCOUNT, name: 'Account Details', description: 'Get account information from indexer', mimeType: 'application/json', schema: accountResourceSchemas[URI_TEMPLATES.INDEXER_ACCOUNT] }, { uri: URI_TEMPLATES.INDEXER_ACCOUNT_TRANSACTIONS, name: 'Transaction History', description: 'Get account transaction history', mimeType: 'application/json', schema: accountResourceSchemas[URI_TEMPLATES.INDEXER_ACCOUNT_TRANSACTIONS] }, { uri: URI_TEMPLATES.INDEXER_ACCOUNT_APPS_LOCAL_STATE, name: 'Application Local States', description: 'Get account application local states', mimeType: 'application/json', schema: accountResourceSchemas[URI_TEMPLATES.INDEXER_ACCOUNT_APPS_LOCAL_STATE] }, { uri: URI_TEMPLATES.INDEXER_ACCOUNT_CREATED_APPS, name: 'Created Applications', description: 'Get applications created by this account', mimeType: 'application/json', schema: accountResourceSchemas[URI_TEMPLATES.INDEXER_ACCOUNT_CREATED_APPS] } ]; export async function lookupAccountByID(address: string): Promise<AccountResponse> { try { const response = await indexerClient.lookupAccountByID(address).do() as AccountResponse; return response; } catch (error) { throw new Error(`Failed to get account info: ${error instanceof Error ? error.message : String(error)}`); } } export async function lookupAccountTransactions(address: string, params?: { limit?: number; beforeTime?: string; afterTime?: string; nextToken?: string; }): Promise<TransactionsResponse> { try { let search = indexerClient.lookupAccountTransactions(address); 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?.nextToken) { search = search.nextToken(params.nextToken); } return await search.do() as TransactionsResponse; } catch (error) { throw new Error(`Failed to get account transactions: ${error instanceof Error ? error.message : String(error)}`); } } export async function lookupAccountAssets(address: string, params?: { limit?: number; assetId?: number; nextToken?: string; }): Promise<AssetHoldingsResponse> { try { let search = indexerClient.lookupAccountAssets(address); if (params?.limit) { search = search.limit(params.limit); } if (params?.assetId) { search = search.assetId(params.assetId); } if (params?.nextToken) { search = search.nextToken(params.nextToken); } return await search.do() as AssetHoldingsResponse; } catch (error) { throw new Error(`Failed to get account assets: ${error instanceof Error ? error.message : String(error)}`); } } export async function lookupAccountAppLocalStates(address: string): Promise<ApplicationLocalStatesResponse> { try { return await indexerClient.lookupAccountAppLocalStates(address).do() as ApplicationLocalStatesResponse; } catch (error) { throw new Error(`Failed to get account application local states: ${error instanceof Error ? error.message : String(error)}`); } } export async function lookupAccountCreatedApplications(address: string): Promise<ApplicationsResponse> { try { return await indexerClient.lookupAccountCreatedApplications(address).do() as ApplicationsResponse; } catch (error) { throw new Error(`Failed to get account created applications: ${error instanceof Error ? error.message : String(error)}`); } } export async function handleAccountResources(uri: string): Promise<ResourceContent[]> { try { // Validate URI format if (!uri.startsWith('algorand://')) { return []; } let match; // Account details match = uri.match(/^algorand:\/\/indexer\/accounts\/([^/]+)$/); if (match) { const address = match[1]; try { const endpoint = mcpUriToEndpoint(uri); const response = await lookupAccountByID(address); return [{ uri, mimeType: 'application/json', text: JSON.stringify({ account: response.account, currentRound: response.currentRound }, null, 2), }]; } catch (error) { console.error('Error fetching account details:', error); throw new McpError( ErrorCode.InternalError, `Failed to get account details: ${error instanceof Error ? error.message : String(error)}` ); } } // Transaction history match = uri.match(/^algorand:\/\/indexer\/accounts\/([^/]+)\/transactions$/); if (match) { const address = match[1]; const history = await lookupAccountTransactions(address); return [{ uri, mimeType: 'application/json', text: JSON.stringify({ transactions: history.transactions, currentRound: history.currentRound }, null, 2), }]; } // Application local states match = uri.match(/^algorand:\/\/indexer\/accounts\/([^/]+)\/apps-local-state$/); if (match) { const address = match[1]; const response = await lookupAccountAppLocalStates(address); return [{ uri, mimeType: 'application/json', text: JSON.stringify({ appsLocalStates: response.appsLocalStates, currentRound: response.currentRound }, null, 2), }]; } // Created applications match = uri.match(/^algorand:\/\/indexer\/accounts\/([^/]+)\/created-applications$/); if (match) { const address = match[1]; const response = await lookupAccountCreatedApplications(address); return [{ uri, mimeType: 'application/json', text: JSON.stringify({ applications: response.applications, currentRound: response.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 searchAccounts(params?: { limit?: number; assetId?: number; applicationId?: number; currencyGreaterThan?: number; currencyLessThan?: number; nextToken?: string; }): Promise<AccountsResponse> { try { let search = indexerClient.searchAccounts(); if (params?.limit) { search = search.limit(params.limit); } if (params?.assetId) { search = search.assetID(params.assetId); } if (params?.applicationId) { search = search.applicationID(params.applicationId); } 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 AccountsResponse; } catch (error) { throw new Error(`Failed to search accounts: ${error instanceof Error ? error.message : String(error)}`); } }