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 { ApplicationResponse, ApplicationsResponse, ApplicationLogsResponse, Box } from 'algosdk/dist/types/client/v2/indexer/models/types'; type ResourceSchema = { type: string; properties: { [key: string]: { type: string; description: string; }; }; }; export const applicationResourceSchemas: { [key: string]: ResourceSchema } = { [URI_TEMPLATES.INDEXER_APPLICATION]: { type: 'object', properties: { application: { type: 'string', description: 'Application information including global and local state' }, currentRound: { type: 'string', description: 'The round at which this information was current' } } }, [URI_TEMPLATES.INDEXER_APPLICATION_LOGS]: { type: 'object', properties: { logs: { type: 'string', description: 'Log messages generated by the application' }, currentRound: { type: 'string', description: 'The round at which this information was current' } } }, [URI_TEMPLATES.INDEXER_APPLICATION_BOX]: { type: 'object', properties: { name: { type: 'string', description: 'Name of the box' }, value: { type: 'string', description: 'Value stored in the box' } } }, [URI_TEMPLATES.INDEXER_APPLICATION_BOXES]: { type: 'object', properties: { boxes: { type: 'string', description: 'List of application boxes' }, nextToken: { type: 'string', description: 'Token for retrieving the next page of results' } } }, [URI_TEMPLATES.INDEXER_APPLICATIONS]: { type: 'object', properties: { applications: { type: 'string', description: 'List of applications matching the search criteria' }, currentRound: { type: 'string', description: 'The round at which this information was current' } } } }; export const applicationResources: ResourceDefinition[] = [ { uri: URI_TEMPLATES.INDEXER_APPLICATION, name: 'Application Details', description: 'Get application information from indexer', mimeType: 'application/json', schema: applicationResourceSchemas[URI_TEMPLATES.INDEXER_APPLICATION] }, { uri: URI_TEMPLATES.INDEXER_APPLICATION_LOGS, name: 'Application Logs', description: 'Get application log messages', mimeType: 'application/json', schema: applicationResourceSchemas[URI_TEMPLATES.INDEXER_APPLICATION_LOGS] }, { uri: URI_TEMPLATES.INDEXER_APPLICATION_BOX, name: 'Application Box', description: 'Get application box by name', mimeType: 'application/json', schema: applicationResourceSchemas[URI_TEMPLATES.INDEXER_APPLICATION_BOX] }, { uri: URI_TEMPLATES.INDEXER_APPLICATION_BOXES, name: 'Application Boxes', description: 'Get all application boxes', mimeType: 'application/json', schema: applicationResourceSchemas[URI_TEMPLATES.INDEXER_APPLICATION_BOXES] }, { uri: URI_TEMPLATES.INDEXER_APPLICATIONS, name: 'Search Applications', description: 'Search for applications with various criteria', mimeType: 'application/json', schema: applicationResourceSchemas[URI_TEMPLATES.INDEXER_APPLICATIONS] } ]; export async function lookupApplications(appId: number): Promise<ApplicationResponse> { try { const response = await indexerClient.lookupApplications(appId).do() as ApplicationResponse; return response; } catch (error) { throw new Error(`Failed to get application info: ${error instanceof Error ? error.message : String(error)}`); } } export async function lookupApplicationLogs(appId: number, params?: { limit?: number; minRound?: number; maxRound?: number; txid?: string; sender?: string; nextToken?: string; }): Promise<ApplicationLogsResponse> { try { let search = indexerClient.lookupApplicationLogs(appId); if (params?.limit) { search = search.limit(params.limit); } if (params?.minRound) { search = search.minRound(params.minRound); } if (params?.maxRound) { search = search.maxRound(params.maxRound); } if (params?.txid) { search = search.txid(params.txid); } if (params?.sender) { search = search.sender(params.sender); } if (params?.nextToken) { search = search.nextToken(params.nextToken); } return await search.do() as ApplicationLogsResponse; } catch (error) { throw new Error(`Failed to get application logs: ${error instanceof Error ? error.message : String(error)}`); } } export async function searchForApplications(params?: { limit?: number; creator?: string; nextToken?: string; }): Promise<ApplicationsResponse> { try { let search = indexerClient.searchForApplications(); if (params?.limit) { search = search.limit(params.limit); } if (params?.creator) { search = search.creator(params.creator); } if (params?.nextToken) { search = search.nextToken(params.nextToken); } return await search.do() as ApplicationsResponse; } catch (error) { throw new Error(`Failed to search applications: ${error instanceof Error ? error.message : String(error)}`); } } export async function getApplicationBoxByName(appId: number, boxName: Uint8Array): Promise<Box> { try { console.log(`Fetching box for application ${appId} with name:`, boxName); const response = await indexerClient.lookupApplicationBoxByIDandName(appId, boxName).do() as Box; console.log('Box response:', JSON.stringify(response, null, 2)); return response; } catch (error) { console.error('Box fetch error:', error); if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Failed to get application box: ${error instanceof Error ? error.message : String(error)}` ); } } export async function handleApplicationResources(uri: string): Promise<ResourceContent[]> { try { // Validate URI format if (!uri.startsWith('algorand://')) { return []; } let match; // Application details match = uri.match(/^algorand:\/\/indexer\/applications\/([^/]+)$/); if (match) { const appId = parseInt(match[1], 10); const info = await lookupApplications(appId); return [{ uri, mimeType: 'application/json', text: JSON.stringify({ application: info.application, currentRound: info.currentRound }, null, 2), }]; } // Application logs match = uri.match(/^algorand:\/\/indexer\/applications\/([^/]+)\/logs$/); if (match) { const appId = parseInt(match[1], 10); const logs = await lookupApplicationLogs(appId); return [{ uri, mimeType: 'application/json', text: JSON.stringify({ logs: logs.logData, currentRound: logs.currentRound }, null, 2), }]; } // Application boxes match = uri.match(/^algorand:\/\/indexer\/applications\/([^/]+)\/boxes$/); if (match) { const appId = parseInt(match[1], 10); const boxes = await indexerClient.searchForApplicationBoxes(appId).do(); return [{ uri, mimeType: 'application/json', text: JSON.stringify(boxes, null, 2), }]; } // Application box by name match = uri.match(/^algorand:\/\/indexer\/applications\/([^/]+)\/box\/([^/]+)$/); if (match) { const [, appId, encodedBoxName] = match; try { // Decode the URI-encoded box name const boxNameWithEncoding = decodeURIComponent(encodedBoxName); // Parse box name format encoding:value const [encoding, value] = boxNameWithEncoding.split(':'); if (!encoding || !value) { throw new McpError( ErrorCode.InvalidRequest, 'Box name must be in format encoding:value' ); } let boxName: Uint8Array; switch (encoding) { case 'str': boxName = new TextEncoder().encode(value); break; case 'int': let intValue = parseInt(value, 10); if (isNaN(intValue)) { throw new McpError( ErrorCode.InvalidRequest, 'Invalid integer value for box name' ); } boxName = new Uint8Array(8); for (let i = 0; i < 8; i++) { boxName[7 - i] = intValue & 0xff; intValue = intValue >> 8; } break; case 'b64': try { boxName = new Uint8Array(Buffer.from(value, 'base64')); } catch (error) { throw new McpError( ErrorCode.InvalidRequest, 'Invalid base64 value for box name' ); } break; case 'addr': if (!algosdk.isValidAddress(value)) { throw new McpError( ErrorCode.InvalidRequest, 'Invalid address value for box name' ); } boxName = new Uint8Array(algosdk.decodeAddress(value).publicKey); break; default: throw new McpError( ErrorCode.InvalidRequest, 'Invalid encoding. Must be one of: str, int, b64, addr' ); } console.log(`Box name decoded from ${encoding}:${value} to:`, boxName); const box = await getApplicationBoxByName(parseInt(appId, 10), boxName); return [{ uri, mimeType: 'application/json', text: JSON.stringify(box, null, 2), }]; } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InvalidRequest, `Invalid box name format: ${error instanceof Error ? error.message : String(error)}` ); } } // Search applications match = uri.match(/^algorand:\/\/indexer\/applications$/); if (match) { const apps = await searchForApplications(); return [{ uri, mimeType: 'application/json', text: JSON.stringify({ applications: apps.applications, currentRound: apps.currentRound }, null, 2), }]; } return []; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to handle resource: ${error instanceof Error ? error.message : String(error)}` ); } }