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, Windsurf, VS Code, Gemini CLI, and OpenAI Codex to perform blockchain operations including account an
448 lines (447 loc) • 21.1 kB
JavaScript
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 "../../server.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()
.default({
pageSize: 50,
}),
}, (_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")}`;
if (nfts.pageKey) {
responseText += `\n\nPage Key: ${nfts.pageKey}`;
}
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()
.default({
maxCount: 50,
}),
}, (_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",
},
],
};
}
// Fetch full transaction data in parallel to get nonce information
const formattedTxs = yield Promise.all(transactions.transfers.map((tx) => __awaiter(this, void 0, void 0, function* () {
try {
const fullTx = yield alchemy.core.getTransaction(tx.hash);
const nonce = (fullTx === null || fullTx === void 0 ? void 0 : fullTx.nonce) !== undefined ? fullTx.nonce : "Unknown";
return `Type: ${tx.category}\nFrom: ${tx.from}\nTo: ${tx.to || "Unknown"}\nValue: ${tx.value || "0"} ${tx.asset || "Unknown"}\nNonce: ${nonce}\nHash: ${tx.hash}\n---`;
}
catch (_err) {
return `Type: ${tx.category}\nFrom: ${tx.from}\nTo: ${tx.to || "Unknown"}\nValue: ${tx.value || "0"} ${tx.asset || "Unknown"}\nNonce: Unknown (failed to fetch)\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)}`,
},
],
};
}
}));
// 6. Account Protocols Analysis
server.tool("getAccountProtocols", "Analyze which smart contracts (protocols) an address interacts with most frequently.", {
address: z
.string()
.describe("The address to analyze for protocol interactions."),
maxResults: z
.number()
.optional()
.default(10)
.describe("Maximum number of top interacted protocol addresses to return. Defaults to 10."),
}, (_a) => __awaiter(this, [_a], void 0, function* ({ address, maxResults }) {
try {
// Helper to fetch transfers (both incoming and outgoing)
const fetchTransfers = (params) => __awaiter(this, void 0, void 0, function* () {
return alchemy.core.getAssetTransfers(Object.assign({ fromBlock: "0x0", maxCount: 1000, category: [
AssetTransfersCategory.EXTERNAL,
AssetTransfersCategory.ERC20,
AssetTransfersCategory.ERC721,
AssetTransfersCategory.ERC1155,
] }, params));
});
// Fetch outgoing and incoming transfers in parallel
const [outgoing, incoming] = yield Promise.all([
fetchTransfers({ fromAddress: address }),
fetchTransfers({ toAddress: address }),
]);
// Count interactions by contract address
const interactionCounts = {};
const addCounter = (addr) => {
if (!addr)
return;
const key = addr.toLowerCase();
if (key === address.toLowerCase())
return; // skip self
interactionCounts[key] = (interactionCounts[key] || 0) + 1;
};
outgoing.transfers.forEach((tx) => addCounter(tx.to));
incoming.transfers.forEach((tx) => addCounter(tx.from));
// Sort by frequency
const sorted = Object.entries(interactionCounts).sort((a, b) => b[1] - a[1]);
const top = sorted.slice(0, maxResults);
// Try to enrich with token metadata when available (best-effort)
const detailed = yield Promise.all(top.map((_a) => __awaiter(this, [_a], void 0, function* ([contractAddress, count]) {
try {
const metadata = yield alchemy.core.getTokenMetadata(contractAddress);
const name = metadata.name || "Unknown Contract";
const symbol = metadata.symbol || "";
return `${name}${symbol ? ` (${symbol})` : ""} - ${contractAddress} : ${count} interactions`;
}
catch (_err) {
return `${contractAddress} : ${count} interactions`;
}
})));
if (detailed.length === 0) {
return {
content: [
{
type: "text",
text: "No protocol interactions found (or insufficient data)",
},
],
};
}
const responseText = `Top protocol / contract interactions for ${address} (last 1000 outgoing + 1000 incoming transfers):\n\n${detailed.join("\n")}`;
return { content: [{ type: "text", text: responseText }] };
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error analyzing protocols: ${handleError(error)}`,
},
],
};
}
}));
}