Alchemy MCP Plugin
by itsanishjain
- dist
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
import { Alchemy, Network, Utils } from "alchemy-sdk";
// Initialize Alchemy SDK with API key from environment variables
const API_KEY = process.env.ALCHEMY_API_KEY;
if (!API_KEY) {
throw new Error("ALCHEMY_API_KEY environment variable is required");
}
console.error("[Setup] Initializing Alchemy MCP server...");
// Get network from environment or default to ETH_MAINNET
const networkStr = process.env.ALCHEMY_NETWORK || "ETH_MAINNET";
const network = Network[networkStr] || Network.ETH_MAINNET;
console.error(`[Setup] Using network: ${networkStr}`);
// Configure Alchemy SDK
const settings = {
apiKey: API_KEY,
network: network,
};
// Create Alchemy instance
const alchemy = new Alchemy(settings);
// Track active subscriptions
const activeSubscriptions = new Map();
// Validation functions (keeping them as they were, just showing a few as example)
const isValidGetNftsForOwnerParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.owner === "string" &&
(args.pageKey === undefined || typeof args.pageKey === "string") &&
(args.pageSize === undefined || typeof args.pageSize === "number") &&
(args.contractAddresses === undefined ||
Array.isArray(args.contractAddresses)) &&
(args.withMetadata === undefined || typeof args.withMetadata === "boolean"));
};
const isValidGetNftMetadataParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.contractAddress === "string" &&
typeof args.tokenId === "string" &&
(args.tokenType === undefined || typeof args.tokenType === "string") &&
(args.refreshCache === undefined || typeof args.refreshCache === "boolean"));
};
const isValidGetTokenBalancesParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.address === "string" &&
(args.tokenAddresses === undefined || Array.isArray(args.tokenAddresses)));
};
const isValidGetAssetTransfersParams = (args) => {
return (typeof args === "object" &&
args !== null &&
(args.fromBlock === undefined || typeof args.fromBlock === "string") &&
(args.toBlock === undefined || typeof args.toBlock === "string") &&
(args.fromAddress === undefined || typeof args.fromAddress === "string") &&
(args.toAddress === undefined || typeof args.toAddress === "string") &&
(args.category === undefined || Array.isArray(args.category)) &&
(args.contractAddresses === undefined ||
Array.isArray(args.contractAddresses)) &&
(args.maxCount === undefined || typeof args.maxCount === "number") &&
(args.excludeZeroValue === undefined ||
typeof args.excludeZeroValue === "boolean") &&
(args.pageKey === undefined || typeof args.pageKey === "string") &&
(args.withMetadata === undefined || typeof args.withMetadata === "boolean"));
};
const isValidGetNftSalesParams = (args) => {
return (typeof args === "object" &&
args !== null &&
(args.contractAddress === undefined ||
typeof args.contractAddress === "string") &&
(args.tokenId === undefined || typeof args.tokenId === "string") &&
(args.fromBlock === undefined || typeof args.fromBlock === "number") &&
(args.toBlock === undefined || typeof args.toBlock === "number") &&
(args.order === undefined || typeof args.order === "string") &&
(args.marketplace === undefined || typeof args.marketplace === "string") &&
(args.pageKey === undefined || typeof args.pageKey === "string") &&
(args.pageSize === undefined || typeof args.pageSize === "number"));
};
const isValidGetContractsForOwnerParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.owner === "string" &&
(args.pageKey === undefined || typeof args.pageKey === "string") &&
(args.pageSize === undefined || typeof args.pageSize === "number") &&
(args.includeFilters === undefined || Array.isArray(args.includeFilters)) &&
(args.excludeFilters === undefined || Array.isArray(args.excludeFilters)));
};
const isValidGetFloorPriceParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.contractAddress === "string");
};
const isValidGetOwnersForNftParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.contractAddress === "string" &&
typeof args.tokenId === "string" &&
(args.pageKey === undefined || typeof args.pageKey === "string") &&
(args.pageSize === undefined || typeof args.pageSize === "number"));
};
const isValidGetTransfersForContractParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.contractAddress === "string" &&
(args.pageKey === undefined || typeof args.pageKey === "string") &&
(args.fromBlock === undefined || typeof args.fromBlock === "number") &&
(args.toBlock === undefined || typeof args.toBlock === "number") &&
(args.order === undefined || typeof args.order === "string") &&
(args.tokenType === undefined || typeof args.tokenType === "string"));
};
const isValidGetTransfersForOwnerParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.owner === "string" &&
(args.pageKey === undefined || typeof args.pageKey === "string") &&
(args.fromBlock === undefined || typeof args.fromBlock === "number") &&
(args.toBlock === undefined || typeof args.toBlock === "number") &&
(args.order === undefined || typeof args.order === "string") &&
(args.tokenType === undefined || typeof args.tokenType === "string") &&
(args.contractAddresses === undefined ||
Array.isArray(args.contractAddresses)));
};
const isValidGetTransactionReceiptsParams = (args) => {
return (typeof args === "object" &&
args !== null &&
(args.blockHash !== undefined || args.blockNumber !== undefined) &&
(args.blockHash === undefined || typeof args.blockHash === "string") &&
(args.blockNumber === undefined ||
typeof args.blockNumber === "string" ||
typeof args.blockNumber === "number"));
};
const isValidGetTokenMetadataParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.contractAddress === "string");
};
const isValidGetTokensForOwnerParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.owner === "string" &&
(args.pageKey === undefined || typeof args.pageKey === "string") &&
(args.pageSize === undefined || typeof args.pageSize === "number") &&
(args.contractAddresses === undefined ||
Array.isArray(args.contractAddresses)));
};
const isValidGetNftsForContractParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.contractAddress === "string" &&
(args.pageKey === undefined || typeof args.pageKey === "string") &&
(args.pageSize === undefined || typeof args.pageSize === "number") &&
(args.tokenUriTimeoutInMs === undefined ||
typeof args.tokenUriTimeoutInMs === "number") &&
(args.withMetadata === undefined || typeof args.withMetadata === "boolean"));
};
const isValidGetBlockWithTransactionsParams = (args) => {
return (typeof args === "object" &&
args !== null &&
(args.blockNumber !== undefined || args.blockHash !== undefined) &&
(args.blockNumber === undefined ||
typeof args.blockNumber === "string" ||
typeof args.blockNumber === "number") &&
(args.blockHash === undefined || typeof args.blockHash === "string"));
};
const isValidGetTransactionParams = (args) => {
return (typeof args === "object" && args !== null && typeof args.hash === "string");
};
const isValidResolveEnsParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.name === "string" &&
(args.blockTag === undefined ||
typeof args.blockTag === "string" ||
typeof args.blockTag === "number"));
};
const isValidLookupAddressParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.address === "string");
};
const isValidEstimateGasPriceParams = (args) => {
return (typeof args === "object" &&
args !== null &&
(args.maxFeePerGas === undefined || typeof args.maxFeePerGas === "boolean"));
};
const isValidSubscribeParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.type === "string" &&
(args.address === undefined || typeof args.address === "string") &&
(args.topics === undefined || Array.isArray(args.topics)));
};
const isValidUnsubscribeParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.subscriptionId === "string");
};
export class AlchemyMcpServer {
server;
alchemy;
activeSubscriptions;
constructor() {
this.server = new Server({
name: "alchemy-sdk-server",
version: "1.0.0",
}, {
capabilities: {
tools: {},
},
});
this.alchemy = alchemy;
this.activeSubscriptions = activeSubscriptions;
this.setupToolHandlers();
this.server.onerror = (error) => console.error("[MCP Error]", error);
process.on("SIGINT", async () => {
for (const [id, subscription] of this.activeSubscriptions.entries()) {
try {
subscription.unsubscribe();
console.error(`[Cleanup] Unsubscribed from subscription ${id}`);
}
catch (error) {
console.error(`[Cleanup] Failed to unsubscribe from ${id}:`, error);
}
}
await this.server.close();
process.exit(0);
});
}
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
// NFT API Tools
{
name: "get_nfts_for_owner",
description: "Get NFTs owned by a specific wallet address",
inputSchema: {
type: "object",
properties: {
owner: {
type: "string",
description: "The wallet address to get NFTs for",
},
pageKey: {
type: "string",
description: "Key for pagination",
},
pageSize: {
type: "number",
description: "Number of NFTs to return in one page (max: 100)",
},
contractAddresses: {
type: "array",
items: {
type: "string",
},
description: "List of contract addresses to filter by",
},
withMetadata: {
type: "boolean",
description: "Whether to include NFT metadata",
},
},
required: ["owner"],
},
},
{
name: "get_nft_metadata",
description: "Get metadata for a specific NFT",
inputSchema: {
type: "object",
properties: {
contractAddress: {
type: "string",
description: "The contract address of the NFT",
},
tokenId: {
type: "string",
description: "The token ID of the NFT",
},
tokenType: {
type: "string",
description: "The token type (ERC721 or ERC1155)",
},
refreshCache: {
type: "boolean",
description: "Whether to refresh the cache",
},
},
required: ["contractAddress", "tokenId"],
},
},
{
name: "get_nft_sales",
description: "Get NFT sales data for a contract or specific NFT",
inputSchema: {
type: "object",
properties: {
contractAddress: {
type: "string",
description: "The contract address of the NFT collection",
},
tokenId: {
type: "string",
description: "The token ID of the specific NFT",
},
fromBlock: {
type: "number",
description: "Starting block number for the query",
},
toBlock: {
type: "number",
description: "Ending block number for the query",
},
order: {
type: "string",
enum: ["asc", "desc"],
description: "Order of results (ascending or descending)",
},
marketplace: {
type: "string",
description: "Filter by marketplace (e.g., 'seaport', 'wyvern')",
},
pageKey: {
type: "string",
description: "Key for pagination",
},
pageSize: {
type: "number",
description: "Number of results per page",
},
},
},
},
{
name: "get_contracts_for_owner",
description: "Get NFT contracts owned by an address",
inputSchema: {
type: "object",
properties: {
owner: {
type: "string",
description: "The wallet address to get contracts for",
},
pageKey: {
type: "string",
description: "Key for pagination",
},
pageSize: {
type: "number",
description: "Number of results per page",
},
includeFilters: {
type: "array",
items: {
type: "string",
enum: ["spam", "airdrops"],
},
description: "Filters to include in the response",
},
excludeFilters: {
type: "array",
items: {
type: "string",
enum: ["spam", "airdrops"],
},
description: "Filters to exclude from the response",
},
},
required: ["owner"],
},
},
{
name: "get_floor_price",
description: "Get floor price for an NFT collection",
inputSchema: {
type: "object",
properties: {
contractAddress: {
type: "string",
description: "The contract address of the NFT collection",
},
},
required: ["contractAddress"],
},
},
{
name: "get_owners_for_nft",
description: "Get owners of a specific NFT",
inputSchema: {
type: "object",
properties: {
contractAddress: {
type: "string",
description: "The contract address of the NFT",
},
tokenId: {
type: "string",
description: "The token ID of the NFT",
},
pageKey: {
type: "string",
description: "Key for pagination",
},
pageSize: {
type: "number",
description: "Number of results per page",
},
},
required: ["contractAddress", "tokenId"],
},
},
{
name: "get_nfts_for_contract",
description: "Get all NFTs for a contract",
inputSchema: {
type: "object",
properties: {
contractAddress: {
type: "string",
description: "The contract address of the NFT collection",
},
pageKey: {
type: "string",
description: "Key for pagination",
},
pageSize: {
type: "number",
description: "Number of results per page",
},
tokenUriTimeoutInMs: {
type: "number",
description: "Timeout for token URI resolution in milliseconds",
},
withMetadata: {
type: "boolean",
description: "Whether to include metadata",
},
},
required: ["contractAddress"],
},
},
{
name: "get_transfers_for_contract",
description: "Get transfers for an NFT contract",
inputSchema: {
type: "object",
properties: {
contractAddress: {
type: "string",
description: "The contract address of the NFT collection",
},
pageKey: {
type: "string",
description: "Key for pagination",
},
fromBlock: {
type: "number",
description: "Starting block number for the query",
},
toBlock: {
type: "number",
description: "Ending block number for the query",
},
order: {
type: "string",
enum: ["asc", "desc"],
description: "Order of results (ascending or descending)",
},
tokenType: {
type: "string",
enum: ["ERC721", "ERC1155"],
description: "Type of token (ERC721 or ERC1155)",
},
},
required: ["contractAddress"],
},
},
{
name: "get_transfers_for_owner",
description: "Get NFT transfers for an owner",
inputSchema: {
type: "object",
properties: {
owner: {
type: "string",
description: "The wallet address to get transfers for",
},
pageKey: {
type: "string",
description: "Key for pagination",
},
fromBlock: {
type: "number",
description: "Starting block number for the query",
},
toBlock: {
type: "number",
description: "Ending block number for the query",
},
order: {
type: "string",
enum: ["asc", "desc"],
description: "Order of results (ascending or descending)",
},
tokenType: {
type: "string",
enum: ["ERC721", "ERC1155"],
description: "Type of token (ERC721 or ERC1155)",
},
contractAddresses: {
type: "array",
items: {
type: "string",
},
description: "List of contract addresses to filter by",
},
},
required: ["owner"],
},
},
// Core API Tools
{
name: "get_token_balances",
description: "Get token balances for a specific address",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "The wallet address to get token balances for",
},
tokenAddresses: {
type: "array",
items: {
type: "string",
},
description: "List of token addresses to filter by",
},
},
required: ["address"],
},
},
{
name: "get_token_metadata",
description: "Get metadata for a token contract",
inputSchema: {
type: "object",
properties: {
contractAddress: {
type: "string",
description: "The contract address of the token",
},
},
required: ["contractAddress"],
},
},
{
name: "get_tokens_for_owner",
description: "Get tokens owned by an address",
inputSchema: {
type: "object",
properties: {
owner: {
type: "string",
description: "The wallet address to get tokens for",
},
pageKey: {
type: "string",
description: "Key for pagination",
},
pageSize: {
type: "number",
description: "Number of results per page",
},
contractAddresses: {
type: "array",
items: {
type: "string",
},
description: "List of contract addresses to filter by",
},
},
required: ["owner"],
},
},
{
name: "get_asset_transfers",
description: "Get asset transfers for a specific address or contract",
inputSchema: {
type: "object",
properties: {
fromBlock: {
type: "string",
description: 'The starting block (hex string or "latest")',
},
toBlock: {
type: "string",
description: 'The ending block (hex string or "latest")',
},
fromAddress: {
type: "string",
description: "The sender address",
},
toAddress: {
type: "string",
description: "The recipient address",
},
category: {
type: "array",
items: {
type: "string",
enum: [
"external",
"internal",
"erc20",
"erc721",
"erc1155",
"specialnft",
],
},
description: 'The category of transfers to include (e.g., "external", "internal", "erc20", "erc721", "erc1155", "specialnft")',
},
contractAddresses: {
type: "array",
items: {
type: "string",
},
description: "List of contract addresses to filter by",
},
maxCount: {
type: "number",
description: "The maximum number of results to return",
},
excludeZeroValue: {
type: "boolean",
description: "Whether to exclude zero value transfers",
},
pageKey: {
type: "string",
description: "Key for pagination",
},
withMetadata: {
type: "boolean",
description: "Whether to include metadata in the response",
},
},
},
},
{
name: "get_transaction_receipts",
description: "Get transaction receipts for a block",
inputSchema: {
type: "object",
properties: {
blockHash: {
type: "string",
description: "The hash of the block",
},
blockNumber: {
type: "string",
description: "The number of the block",
},
},
oneOf: [{ required: ["blockHash"] }, { required: ["blockNumber"] }],
},
},
{
name: "get_block_number",
description: "Get the latest block number",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "get_block_with_transactions",
description: "Get a block with its transactions",
inputSchema: {
type: "object",
properties: {
blockNumber: {
type: "string",
description: "The block number",
},
blockHash: {
type: "string",
description: "The block hash",
},
},
oneOf: [{ required: ["blockNumber"] }, { required: ["blockHash"] }],
},
},
{
name: "get_transaction",
description: "Get transaction details by hash",
inputSchema: {
type: "object",
properties: {
hash: {
type: "string",
description: "The transaction hash",
},
},
required: ["hash"],
},
},
{
name: "resolve_ens",
description: "Resolve an ENS name to an address",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "The ENS name to resolve",
},
blockTag: {
type: "string",
description: "The block tag to use for resolution",
},
},
required: ["name"],
},
},
{
name: "lookup_address",
description: "Lookup the ENS name for an address",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "The address to lookup",
},
},
required: ["address"],
},
},
{
name: "estimate_gas_price",
description: "Estimate current gas price",
inputSchema: {
type: "object",
properties: {
maxFeePerGas: {
type: "boolean",
description: "Whether to include maxFeePerGas and maxPriorityFeePerGas",
},
},
},
},
// WebSocket Subscription Tools
{
name: "subscribe",
description: "Subscribe to blockchain events",
inputSchema: {
type: "object",
properties: {
type: {
type: "string",
enum: ["newHeads", "logs", "pendingTransactions", "mined"],
description: "The type of subscription",
},
address: {
type: "string",
description: "The address to filter by (for logs)",
},
topics: {
type: "array",
items: {
type: "string",
},
description: "The topics to filter by (for logs)",
},
},
required: ["type"],
},
},
{
name: "unsubscribe",
description: "Unsubscribe from blockchain events",
inputSchema: {
type: "object",
properties: {
subscriptionId: {
type: "string",
description: "The ID of the subscription to cancel",
},
},
required: ["subscriptionId"],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
if (!request.params.arguments) {
throw new McpError(ErrorCode.InvalidParams, "Missing arguments");
}
let result;
switch (request.params.name) {
case "get_nfts_for_owner":
result = await this.handleGetNftsForOwner(request.params.arguments);
break;
case "get_nft_metadata":
result = await this.handleGetNftMetadata(request.params.arguments);
break;
// ... (other cases remain the same)
case "estimate_gas_price":
result = await this.handleEstimateGasPrice(request.params.arguments);
break;
case "subscribe":
result = await this.handleSubscribe(request.params.arguments);
break;
case "unsubscribe":
result = await this.handleUnsubscribe(request.params.arguments);
break;
default:
throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);
}
return {
content: [
{
type: "text",
text: JSON.stringify(result),
},
],
};
}
catch (error) {
console.error("[Tool Error]", error);
throw new McpError(ErrorCode.InternalError, `Tool error: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
validateAndCastParams(args, validator, errorMessage) {
if (!validator(args)) {
throw new McpError(ErrorCode.InvalidParams, errorMessage);
}
return args;
}
isValidEstimateGasPriceParams = (args) => {
return (typeof args === "object" &&
args !== null &&
(args.maxFeePerGas === undefined ||
typeof args.maxFeePerGas === "boolean"));
};
isValidSubscribeParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.type === "string" &&
["newHeads", "logs", "pendingTransactions", "mined"].includes(args.type) &&
(args.address === undefined || typeof args.address === "string") &&
(args.topics === undefined || Array.isArray(args.topics)));
};
isValidUnsubscribeParams = (args) => {
return (typeof args === "object" &&
args !== null &&
typeof args.subscriptionId === "string");
};
// Then in your AlchemyMcpServer class, make sure these handlers are included:
async handleEstimateGasPrice(args) {
const params = this.validateAndCastParams(args, isValidEstimateGasPriceParams, "Invalid gas price parameters");
const gasPrice = await this.alchemy.core.getGasPrice();
return params.maxFeePerGas
? { gasPrice: Utils.formatUnits(gasPrice, "gwei") }
: { gasPrice };
}
async handleSubscribe(args) {
const params = this.validateAndCastParams(args, isValidSubscribeParams, "Invalid subscribe parameters");
const subscriptionId = Math.random().toString(36).substring(7);
let subscription;
switch (params.type) {
case "newHeads":
subscription = this.alchemy.ws.on("block", (blockNumber) => {
console.log("[WebSocket] New block:", blockNumber);
});
break;
case "logs":
subscription = this.alchemy.ws.on({
address: params.address,
topics: params.topics,
}, (log) => {
console.log("[WebSocket] New log:", log);
});
break;
case "pendingTransactions":
subscription = this.alchemy.ws.on("pending", (tx) => {
console.log("[WebSocket] Pending transaction:", tx);
});
break;
case "mined":
subscription = this.alchemy.ws.on("mined", (tx) => {
console.log("[WebSocket] Mined transaction:", tx);
});
break;
default:
throw new McpError(ErrorCode.InvalidParams, `Unknown subscription type: ${params.type}`);
}
this.activeSubscriptions.set(subscriptionId, subscription);
return { subscriptionId };
}
async handleUnsubscribe(args) {
const params = this.validateAndCastParams(args, isValidUnsubscribeParams, "Invalid unsubscribe parameters");
const subscription = this.activeSubscriptions.get(params.subscriptionId);
if (!subscription) {
throw new McpError(ErrorCode.InvalidParams, `Subscription not found: ${params.subscriptionId}`);
}
subscription.unsubscribe();
this.activeSubscriptions.delete(params.subscriptionId);
return { success: true };
}
async handleGetNftsForOwner(args) {
const params = this.validateAndCastParams(args, isValidGetNftsForOwnerParams, "Invalid NFTs for owner parameters");
return await this.alchemy.nft.getNftsForOwner(params.owner, params);
}
async handleGetNftMetadata(args) {
const params = this.validateAndCastParams(args, isValidGetNftMetadataParams, "Invalid NFT metadata parameters");
return await this.alchemy.nft.getNftMetadata(params.contractAddress, params.tokenId, params);
}
async start() {
try {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("[Setup] Alchemy MCP server started");
}
catch (error) {
console.error("[Server Start Error]", error);
throw error; // or handle it differently based on your needs
}
}
}
// Start the server
const server = new AlchemyMcpServer();
server.start().catch((error) => {
console.error("[Fatal Error]", error);
process.exit(1);
});