UNPKG

arbitrum-mcp-tools

Version:

A comprehensive collection of Model Context Protocol (MCP) tools for interacting with the Arbitrum blockchain. Enables AI assistants like Claude, Cursor, and Windsurf to perform blockchain operations including account analysis, contract interaction, cross

352 lines (351 loc) 16.4 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { z } from "zod"; import { formatEther } from "ethers"; import { formatTokenBalance, handleError } from "../common.js"; import { alchemy } from "../../index.js"; import { AssetTransfersCategory, TokenBalanceType, NftFilters, NftOrdering, NftTokenType, } from "alchemy-sdk"; export function registerAccountAnalysisTools(server) { // 1. Balance server.tool("getAccountBalance", "Get native token balance for an Arbitrum address", { address: z.string().describe("Ethereum address to check balance for"), blockTag: z .string() .optional() .describe("The optional block number, hash, or tag (e.g., 'latest', 'pending', 'safe', 'finalized', 'earliest') to get the balance for. Defaults to 'latest' if unspecified."), }, (_a) => __awaiter(this, [_a], void 0, function* ({ address, blockTag }) { try { const balance = yield alchemy.core.getBalance(address, blockTag || "latest"); return { content: [ { type: "text", text: `Balance: ${formatEther(balance.toString())} ETH`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error fetching balance: ${handleError(error)}`, }, ], }; } })); // 2. Token Balances server.tool("getTokenBalances", "Get ERC-20 token balances for an Arbitrum address, optionally filtered by a list of contract addresses.", { address: z .string() .describe("The owner address or ENS name to get the token balances for."), contractAddresses: z .array(z.string()) .optional() .describe("Optional list of contract addresses to filter by."), }, (_a) => __awaiter(this, [_a], void 0, function* ({ address, contractAddresses }) { try { let balances; if (contractAddresses && contractAddresses.length > 0) { balances = yield alchemy.core.getTokenBalances(address, contractAddresses); } else { balances = yield alchemy.core.getTokenBalances(address, { type: TokenBalanceType.ERC20, }); } if (balances.tokenBalances.length === 0) { return { content: [ { type: "text", text: "No token balances found", }, ], }; } const formattedBalances = yield Promise.all(balances.tokenBalances.map((token) => __awaiter(this, void 0, void 0, function* () { const metadata = yield alchemy.core.getTokenMetadata(token.contractAddress); const balance = formatTokenBalance(BigInt(token.tokenBalance || "0"), metadata.decimals || 18); const name = metadata.name || "Unknown Token"; const symbol = metadata.symbol || "???"; return `${name} (${symbol}): ${balance}`; }))); let responseText = `Token balances for ${address}:\n\n${formattedBalances.join("\n")}`; return { content: [ { type: "text", text: responseText, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error fetching token balances: ${handleError(error)}`, }, ], }; } })); // 3. NFTs server.tool("getNfts", "Get NFTs owned by an address, with options for filtering, pagination, and ordering.", { owner: z.string().describe("The address of the owner."), options: z .object({ contractAddresses: z .array(z.string()) .optional() .describe("Optional list of contract addresses to filter the results by. Limit is 45."), omitMetadata: z .boolean() .optional() .default(false) .describe("Optional boolean flag to omit NFT metadata. Defaults to false."), excludeFilters: z .array(z.enum([NftFilters.SPAM, NftFilters.AIRDROPS])) .optional() .describe("Optional list of filters applied to the query. NFTs that match one or more of these filters are excluded from the response."), includeFilters: z .array(z.enum([NftFilters.SPAM, NftFilters.AIRDROPS])) .optional() .describe("Optional list of filters applied to the query. NFTs that match one or more of these filters are included in the response."), pageSize: z .number() .optional() .default(50) .describe("Sets the total number of NFTs to return in the response. API default is 50. Maximum page size is 100."), tokenUriTimeoutInMs: z .number() .optional() .describe("No set timeout by default - When metadata is requested, this parameter is the timeout (in milliseconds) for the website hosting the metadata to respond. If you want to only access the cache and not live fetch any metadata for cache misses then set this value to 0."), orderBy: z .enum([NftOrdering.TRANSFERTIME]) .optional() .describe("Order in which to return results. By default, results are ordered by contract address and token ID in lexicographic order. The available option is TRANSFERTIME."), pageKey: z .string() .optional() .describe("Optional page key to use for pagination."), }) .optional(), }, (_a) => __awaiter(this, [_a], void 0, function* ({ owner, options }) { try { const nfts = yield alchemy.nft.getNftsForOwner(owner, options ? Object.assign({}, options) : undefined); if (nfts.totalCount === 0) { return { content: [ { type: "text", text: "No NFTs found", }, ], }; } const formattedNfts = nfts.ownedNfts.map((nft) => `Collection: ${nft.contract.name || "Unknown Collection"}\nToken ID: ${nft.tokenId}\nType: ${nft.tokenType}\n---`); let responseText = `NFTs owned by ${owner}:\n\n${formattedNfts.join("\n")}`; return { content: [ { type: "text", text: responseText, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error fetching NFTs: ${handleError(error)}`, }, ], }; } })); // 4. Transaction History server.tool("getTransactionHistory", "Get transaction history for an Arbitrum address, with options for filtering and pagination.", { address: z .string() .describe("The address to check transactions for (used as fromAddress)."), options: z .object({ fromBlock: z .string() .optional() .describe('The starting block to check for transfers. Defaults to "0x0".'), toBlock: z .string() .optional() .describe('Inclusive to block (hex string, int, or latest). Defaults to "latest".'), toAddress: z .string() .optional() .describe("The recipient address to filter transfers by. Defaults to a wildcard."), contractAddresses: z .array(z.string()) .optional() .describe("List of contract addresses to filter for - only applies to erc20, erc721, erc1155 transfers. Defaults to all addresses if omitted."), excludeZeroValue: z .boolean() .optional() .describe("Whether to exclude transfers with zero value. API Defaults to true."), order: z .enum(["asc", "desc"]) .optional() .describe("Whether to return results in ascending or descending order by block number (asc/desc). API Defaults to ascending."), category: z .array(z.enum([ AssetTransfersCategory.EXTERNAL, AssetTransfersCategory.INTERNAL, AssetTransfersCategory.ERC20, AssetTransfersCategory.ERC721, AssetTransfersCategory.ERC1155, AssetTransfersCategory.SPECIALNFT, ])) .optional() .describe("An array of categories to get transfers for. API defaults to all if omitted."), maxCount: z .number() .optional() .default(50) .describe("The maximum number of results to return per page. API Defaults to 1000 if omitted."), withMetadata: z .boolean() .optional() .describe("Whether to include additional metadata about each transfer event. API Defaults to false."), pageKey: z .string() .optional() .describe("Optional page key from an existing one to use for pagination."), }) .optional(), }, (_a) => __awaiter(this, [_a], void 0, function* ({ address, options }) { var _b, _c; try { const baseParams = { fromAddress: address, fromBlock: (_b = options === null || options === void 0 ? void 0 : options.fromBlock) !== null && _b !== void 0 ? _b : "0x0", category: (_c = options === null || options === void 0 ? void 0 : options.category) !== null && _c !== void 0 ? _c : [ AssetTransfersCategory.EXTERNAL, AssetTransfersCategory.ERC20, AssetTransfersCategory.ERC721, AssetTransfersCategory.ERC1155, ], }; if ((options === null || options === void 0 ? void 0 : options.toBlock) !== undefined) baseParams.toBlock = options.toBlock; if ((options === null || options === void 0 ? void 0 : options.toAddress) !== undefined) baseParams.toAddress = options.toAddress; if ((options === null || options === void 0 ? void 0 : options.contractAddresses) !== undefined) baseParams.contractAddresses = options.contractAddresses; if ((options === null || options === void 0 ? void 0 : options.excludeZeroValue) !== undefined) baseParams.excludeZeroValue = options.excludeZeroValue; if ((options === null || options === void 0 ? void 0 : options.order) !== undefined) baseParams.order = options.order; if ((options === null || options === void 0 ? void 0 : options.maxCount) !== undefined) baseParams.maxCount = options.maxCount; if ((options === null || options === void 0 ? void 0 : options.pageKey) !== undefined) baseParams.pageKey = options.pageKey; let finalParams = baseParams; if ((options === null || options === void 0 ? void 0 : options.withMetadata) === true) { finalParams = Object.assign(Object.assign({}, baseParams), { withMetadata: true }); } const transactions = yield alchemy.core.getAssetTransfers(finalParams); if (transactions.transfers.length === 0) { return { content: [ { type: "text", text: "No transactions found", }, ], }; } const formattedTxs = transactions.transfers.map((tx) => `Type: ${tx.category}\nFrom: ${tx.from}\nTo: ${tx.to || "Unknown"}\nValue: ${tx.value || "0"} ${tx.asset || "Unknown"}\nHash: ${tx.hash}\n---`); let responseText = `Transaction history for ${address}:\n\n${formattedTxs.join("\n")}`; if (transactions.pageKey) { responseText += `\n\nPage Key: ${transactions.pageKey}`; } return { content: [ { type: "text", text: responseText, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error fetching transaction history: ${handleError(error)}`, }, ], }; } })); // 5. NFT Metadata server.tool("getNftMetadata", "Get metadata for an NFT given its contract address and token ID, with options for caching and token type.", { contractAddress: z.string().describe("NFT contract address"), tokenId: z.string().describe("Token ID"), options: z .object({ tokenType: z .enum([ NftTokenType.ERC721, NftTokenType.ERC1155, NftTokenType.UNKNOWN, ]) .optional() .describe("Optional field to specify the type of token to speed up the query."), tokenUriTimeoutInMs: z .number() .optional() .describe("No set timeout by default - When metadata is requested, this parameter is the timeout (in milliseconds) for the website hosting the metadata to respond. If you want to only access the cache and not live fetch any metadata for cache misses then set this value to 0."), refreshCache: z .boolean() .optional() .describe("Whether to refresh the metadata for the given NFT token before returning the response. API Defaults to false for faster response times."), }) .optional(), }, (_a) => __awaiter(this, [_a], void 0, function* ({ contractAddress, tokenId, options }) { try { const metadata = yield alchemy.nft.getNftMetadata(contractAddress, tokenId, options ? Object.assign({}, options) : undefined); return { content: [ { type: "text", text: `NFT Metadata:\n${JSON.stringify(metadata, null, 2)}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${handleError(error)}`, }, ], }; } })); }