# Directory Structure
```
├── .clinerules
├── .gitignore
├── dist
│ └── index.js
├── Dockerfile
├── index.ts
├── package-lock.json
├── package.json
├── README.md
├── settings.json
├── smithery.yaml
├── test-alchemy.js
├── test-eth-price.js
├── tsconfig.json
├── views
│ └── index.ejs
├── yarn.lock
└── zerops.yml
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://docs.npmjs.com/cli/shrinkwrap#caveats
node_modules
# Debug log from npm
npm-debug.log
.DS_Store
/node_modules
database.sqlite
database.sqlite-journal
db.sqlite3
```
--------------------------------------------------------------------------------
/.clinerules:
--------------------------------------------------------------------------------
```
# MCP Plugin Development Protocol
⚠️ CRITICAL: DO NOT USE attempt_completion BEFORE TESTING ⚠️
## Step 1: Planning (PLAN MODE)
- What problem does this tool solve?
- What API/service will it use?
- What are the authentication requirements?
□ Standard API key
□ OAuth (requires separate setup script)
□ Other credentials
## Step 2: Implementation (ACT MODE)
1. Bootstrap
- For web services, JavaScript integration, or Node.js environments:
```bash
npx @modelcontextprotocol/create-server my-server
cd my-server
npm install
```
- For data science, ML workflows, or Python environments:
```bash
pip install mcp
# Or with uv (recommended)
uv add "mcp[cli]"
```
2. Core Implementation
- Use MCP SDK
- Implement comprehensive logging
- TypeScript (for web/JS projects):
```typescript
console.error('[Setup] Initializing server...');
console.error('[API] Request to endpoint:', endpoint);
console.error('[Error] Failed with:', error);
```
- Python (for data science/ML projects):
```python
import logging
logging.error('[Setup] Initializing server...')
logging.error(f'[API] Request to endpoint: {endpoint}')
logging.error(f'[Error] Failed with: {str(error)}')
```
- Add type definitions
- Handle errors with context
- Implement rate limiting if needed
3. Configuration
- Get credentials from user if needed
- Add to MCP settings:
- For TypeScript projects:
```json
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["path/to/build/index.js"],
"env": {
"API_KEY": "key"
},
"disabled": false,
"autoApprove": []
}
}
}
```
- For Python projects:
```bash
# Directly with command line
mcp install server.py -v API_KEY=key
# Or in settings.json
{
"mcpServers": {
"my-server": {
"command": "python",
"args": ["server.py"],
"env": {
"API_KEY": "key"
},
"disabled": false,
"autoApprove": []
}
}
}
```
## Step 3: Testing (BLOCKER ⛔️)
<thinking>
BEFORE using attempt_completion, I MUST verify:
□ Have I tested EVERY tool?
□ Have I confirmed success from the user for each test?
□ Have I documented the test results?
If ANY answer is "no", I MUST NOT use attempt_completion.
</thinking>
1. Test Each Tool (REQUIRED)
□ Test each tool with valid inputs
□ Verify output format is correct
⚠️ DO NOT PROCEED UNTIL ALL TOOLS TESTED
## Step 4: Completion
❗ STOP AND VERIFY:
□ Every tool has been tested with valid inputs
□ Output format is correct for each tool
Only after ALL tools have been tested can attempt_completion be used.
## Key Requirements
- ✓ Must use MCP SDK
- ✓ Must have comprehensive logging
- ✓ Must test each tool individually
- ✓ Must handle errors gracefully
- ⛔️ NEVER skip testing before completion
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Alchemy MCP Plugin
[](https://smithery.ai/server/@itsanishjain/alchemy-sdk-mcp)
This MCP plugin provides integration with the Alchemy SDK for blockchain and NFT operations.
## Features
- Get NFTs for a wallet address
- Get NFT metadata
- Get latest block number
- More endpoints can be added as needed
## Setup
### Installing via Smithery
To install alchemy-sdk-mcp for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@itsanishjain/alchemy-sdk-mcp):
```bash
npx -y @smithery/cli install @itsanishjain/alchemy-sdk-mcp --client claude
```
### Manual Installation
1. Install dependencies:
```bash
npm install
```
2. Build the project:
```bash
npm run build
```
3. Configure your Alchemy API key:
- Get an API key from [Alchemy](https://www.alchemy.com/)
- Update the `ALCHEMY_API_KEY` in `settings.json`
4. Start the server:
```bash
npm start
```
## Available Endpoints
### 1. Get NFTs for Owner
```typescript
POST /getNftsForOwner
{
"owner": "wallet_address"
}
```
### 2. Get NFT Metadata
```typescript
POST /getNftMetadata
{
"contractAddress": "contract_address",
"tokenId": "token_id"
}
```
### 3. Get Block Number
```typescript
POST /getBlockNumber
```
## Error Handling
All endpoints include proper error handling and logging. Errors are returned in the format:
```json
{
"error": "Error message"
}
```
## Logging
The server implements comprehensive logging using console.error for better debugging:
- [Setup] logs for initialization
- [API] logs for API calls
- [Error] logs for error handling
$env:ALCHEMY_API_KEY="KRdhdsBezoTMVajIknIxlXgBHc1Pprpw"; node dist/index.js
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true
}
}
```
--------------------------------------------------------------------------------
/settings.json:
--------------------------------------------------------------------------------
```json
{
"mcpServers": {
"alchemy-mcp": {
"command": "node",
"args": ["dist/index.js"],
"env": {
"ALCHEMY_API_KEY": "KRdhdsBezoTMVajIknIxlXgBHc1Pprpw",
"PORT": "3000"
},
"disabled": false,
"autoApprove": []
}
}
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "alchemy-mcp",
"type": "module",
"version": "1.0.0",
"description": "MCP plugin for Alchemy SDK",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.1",
"alchemy-sdk": "^2.11.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.19.3",
"typescript": "^5.0.0"
}
}
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:lts-alpine
# Create and set working directory
WORKDIR /app
# Copy package files and install dependencies
COPY package*.json ./
RUN npm install --ignore-scripts
# Copy the rest of the application
COPY . .
# Build the project
RUN npm run build
# Optionally expose a port if needed (the settings.json indicates port 3000, but the server communicates over stdio)
# EXPOSE 3000
# Start the MCP server
CMD [ "npm", "start" ]
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
startCommand:
type: stdio
configSchema:
# JSON Schema defining the configuration options for the MCP.
type: object
required:
- alchemyApiKey
properties:
alchemyApiKey:
type: string
description: Your Alchemy API key for accessing the Alchemy SDK services.
commandFunction:
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|-
(config) => ({
command: 'node',
args: ['dist/index.js'],
env: {
ALCHEMY_API_KEY: config.alchemyApiKey
}
})
exampleConfig:
alchemyApiKey: your_dummy_alchemy_api_key_here
```
--------------------------------------------------------------------------------
/test-alchemy.js:
--------------------------------------------------------------------------------
```javascript
import { Alchemy, Network } from "alchemy-sdk";
// Initialize Alchemy SDK with API key
const API_KEY = "KRdhdsBezoTMVajIknIxlXgBHc1Pprpw";
// Configure Alchemy SDK
const settings = {
apiKey: API_KEY,
network: Network.ETH_MAINNET,
};
// Create Alchemy instance
const alchemy = new Alchemy(settings);
async function testAlchemy() {
try {
console.log("Testing Alchemy API connection...");
// Get current gas price
const gasPrice = await alchemy.core.getGasPrice();
console.log("Current gas price (wei):", gasPrice.toString());
console.log(
"Current gas price (gwei):",
parseInt(gasPrice.toString()) / 1e9
);
// Get latest block number
const blockNumber = await alchemy.core.getBlockNumber();
console.log("Latest block number:", blockNumber);
console.log("Alchemy API connection test successful!");
} catch (error) {
console.error("Error testing Alchemy API:", error);
}
}
testAlchemy();
```
--------------------------------------------------------------------------------
/zerops.yml:
--------------------------------------------------------------------------------
```yaml
zerops:
- setup: alpine0
# ==== how to build your application ====
build:
# what technologies should the build
# container be based on (can be an array)
os: alpine
base: nodejs@20
# what commands to use to build your app
buildCommands:
- npm i
# select which files / folders to deploy
# after the build succesfully finished
deployFiles:
- ./
# *optional*: which files / folders
# to cache for the next build run
cache:
- node_modules
- yarn.lock
# ==== how to run your application ====
run:
# what technology should the runtime
# container be based on, can be extended
# in `run.prepareCommands` using
# `zsc install nodejs@20`
base: nodejs@20
os: alpine
envVariables:
PORT: "8081"
# what ports your app listens on
# and whether it supports http traffic
ports:
- port: 8081
httpSupport: true
# how to start your application
start: npm start
```
--------------------------------------------------------------------------------
/test-eth-price.js:
--------------------------------------------------------------------------------
```javascript
import { Alchemy, Network, Utils } from "alchemy-sdk";
// Initialize Alchemy SDK with API key
const API_KEY = "KRdhdsBezoTMVajIknIxlXgBHc1Pprpw";
// Configure Alchemy SDK
const settings = {
apiKey: API_KEY,
network: Network.ETH_MAINNET,
};
// Create Alchemy instance
const alchemy = new Alchemy(settings);
// USDC contract address (a stable coin pegged to USD)
const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
// WETH contract address (Wrapped ETH)
const WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
async function getEthPrice() {
try {
console.log("Fetching ETH price...");
// Method 1: Using Alchemy's getTokenBalances to check WETH/USDC ratio
// This is a simplified approach and not the most accurate for price data
console.log("Method 1: Using token balances (simplified approach)");
// Get latest block number for reference
const blockNumber = await alchemy.core.getBlockNumber();
console.log("Latest block number:", blockNumber);
// Get gas price as a basic test of Alchemy connection
const gasPrice = await alchemy.core.getGasPrice();
console.log("Current gas price (wei):", gasPrice.toString());
console.log(
"Current gas price (gwei):",
parseInt(gasPrice.toString()) / 1e9
);
// Note: For accurate ETH price, you would typically:
// 1. Query a price oracle like Chainlink
// 2. Check a DEX like Uniswap for the ETH/USDC pair
// 3. Use a price API service
console.log("\nFor accurate ETH price data, consider:");
console.log("1. Adding a Chainlink price feed oracle integration");
console.log("2. Querying Uniswap or another DEX for the ETH/USDC pair");
console.log("3. Using a price API service like CoinGecko or CryptoCompare");
// Example of what the implementation might look like:
console.log("\nExample implementation (pseudocode):");
console.log(`
// Using Chainlink ETH/USD Price Feed
const ETH_USD_PRICE_FEED = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419";
const aggregatorV3InterfaceABI = [...]; // ABI for price feed
const priceFeedContract = new ethers.Contract(
ETH_USD_PRICE_FEED,
aggregatorV3InterfaceABI,
provider
);
const roundData = await priceFeedContract.latestRoundData();
const price = roundData.answer.toString() / 10**8; // Adjust for decimals
console.log("ETH price (USD):", price);
`);
} catch (error) {
console.error("Error fetching ETH price:", error);
}
}
getEthPrice();
```
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
```typescript
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 as keyof typeof Network] || 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: Map<string, { unsubscribe: () => void }> = new Map();
// Import types from alchemy-sdk
import type {
GetNftsForOwnerOptions,
GetNftMetadataOptions,
AssetTransfersParams,
GetNftSalesOptions,
GetContractsForOwnerOptions,
GetOwnersForNftOptions,
GetTransfersForContractOptions,
GetTransfersForOwnerOptions,
TransactionReceiptsParams,
GetTokensForOwnerOptions,
GetBaseNftsForContractOptions,
} from "alchemy-sdk";
// Parameter type definitions
type GetNftsForOwnerParams = GetNftsForOwnerOptions & { owner: string };
type GetNftMetadataParams = GetNftMetadataOptions & {
contractAddress: string;
tokenId: string;
};
type GetTokenBalancesParams = { address: string; tokenAddresses?: string[] };
type GetAssetTransfersParams = AssetTransfersParams;
type GetNftSalesParams = GetNftSalesOptions & {
contractAddress?: string;
tokenId?: string;
};
type GetContractsForOwnerParams = GetContractsForOwnerOptions & {
owner: string;
};
type GetFloorPriceParams = { contractAddress: string };
type GetOwnersForNftParams = GetOwnersForNftOptions & {
contractAddress: string;
tokenId: string;
};
type GetTransfersForContractParams = GetTransfersForContractOptions & {
contractAddress: string;
};
type GetTransfersForOwnerParams = GetTransfersForOwnerOptions & {
owner: string;
};
type GetTransactionReceiptsParams = TransactionReceiptsParams;
type GetTokenMetadataParams = { contractAddress: string };
type GetTokensForOwnerParams = GetTokensForOwnerOptions & { owner: string };
type GetNftsForContractParams = GetBaseNftsForContractOptions & {
contractAddress: string;
};
type GetBlockWithTransactionsParams = {
blockNumber?: string | number;
blockHash?: string;
};
type GetTransactionParams = { hash: string };
type ResolveEnsParams = { name: string; blockTag?: string | number };
type LookupAddressParams = { address: string };
type EstimateGasPriceParams = { maxFeePerGas?: boolean };
type SubscribeParams = {
type: string;
address?: string;
topics?: string[];
};
type UnsubscribeParams = {
subscriptionId: string;
};
// Validation functions (keeping them as they were, just showing a few as example)
const isValidGetNftsForOwnerParams = (
args: any
): args is GetNftsForOwnerParams => {
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: any
): args is GetNftMetadataParams => {
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: any
): args is GetTokenBalancesParams => {
return (
typeof args === "object" &&
args !== null &&
typeof args.address === "string" &&
(args.tokenAddresses === undefined || Array.isArray(args.tokenAddresses))
);
};
const isValidGetAssetTransfersParams = (
args: any
): args is GetAssetTransfersParams => {
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: any): args is GetNftSalesParams => {
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: any
): args is GetContractsForOwnerParams => {
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: any): args is GetFloorPriceParams => {
return (
typeof args === "object" &&
args !== null &&
typeof args.contractAddress === "string"
);
};
const isValidGetOwnersForNftParams = (
args: any
): args is GetOwnersForNftParams => {
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: any
): args is GetTransfersForContractParams => {
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: any
): args is GetTransfersForOwnerParams => {
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: any
): args is GetTransactionReceiptsParams => {
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: any
): args is GetTokenMetadataParams => {
return (
typeof args === "object" &&
args !== null &&
typeof args.contractAddress === "string"
);
};
const isValidGetTokensForOwnerParams = (
args: any
): args is GetTokensForOwnerParams => {
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: any
): args is GetNftsForContractParams => {
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: any
): args is GetBlockWithTransactionsParams => {
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: any
): args is GetTransactionParams => {
return (
typeof args === "object" && args !== null && typeof args.hash === "string"
);
};
const isValidResolveEnsParams = (args: any): args is ResolveEnsParams => {
return (
typeof args === "object" &&
args !== null &&
typeof args.name === "string" &&
(args.blockTag === undefined ||
typeof args.blockTag === "string" ||
typeof args.blockTag === "number")
);
};
const isValidLookupAddressParams = (args: any): args is LookupAddressParams => {
return (
typeof args === "object" &&
args !== null &&
typeof args.address === "string"
);
};
const isValidEstimateGasPriceParams = (
args: any
): args is EstimateGasPriceParams => {
return (
typeof args === "object" &&
args !== null &&
(args.maxFeePerGas === undefined || typeof args.maxFeePerGas === "boolean")
);
};
const isValidSubscribeParams = (args: any): args is SubscribeParams => {
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: any): args is UnsubscribeParams => {
return (
typeof args === "object" &&
args !== null &&
typeof args.subscriptionId === "string"
);
};
export class AlchemyMcpServer {
private server: Server;
private alchemy: Alchemy;
private activeSubscriptions: Map<string, { unsubscribe: () => void }>;
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);
});
}
private 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: unknown;
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)
}`
);
}
});
}
private validateAndCastParams<T>(
args: Record<string, unknown>,
validator: (args: any) => boolean,
errorMessage: string
): T {
if (!validator(args)) {
throw new McpError(ErrorCode.InvalidParams, errorMessage);
}
return args as T;
}
isValidEstimateGasPriceParams = (
args: any
): args is EstimateGasPriceParams => {
return (
typeof args === "object" &&
args !== null &&
(args.maxFeePerGas === undefined ||
typeof args.maxFeePerGas === "boolean")
);
};
isValidSubscribeParams = (args: any): args is SubscribeParams => {
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: any): args is UnsubscribeParams => {
return (
typeof args === "object" &&
args !== null &&
typeof args.subscriptionId === "string"
);
};
// Then in your AlchemyMcpServer class, make sure these handlers are included:
private async handleEstimateGasPrice(args: Record<string, unknown>) {
const params = this.validateAndCastParams<EstimateGasPriceParams>(
args,
isValidEstimateGasPriceParams,
"Invalid gas price parameters"
);
const gasPrice = await this.alchemy.core.getGasPrice();
return params.maxFeePerGas
? { gasPrice: Utils.formatUnits(gasPrice, "gwei") }
: { gasPrice };
}
private async handleSubscribe(args: Record<string, unknown>) {
const params = this.validateAndCastParams<SubscribeParams>(
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 };
}
private async handleUnsubscribe(args: Record<string, unknown>) {
const params = this.validateAndCastParams<UnsubscribeParams>(
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 };
}
private async handleGetNftsForOwner(args: Record<string, unknown>) {
const params = this.validateAndCastParams<GetNftsForOwnerParams>(
args,
isValidGetNftsForOwnerParams,
"Invalid NFTs for owner parameters"
);
return await this.alchemy.nft.getNftsForOwner(params.owner, params);
}
private async handleGetNftMetadata(args: Record<string, unknown>) {
const params = this.validateAndCastParams<GetNftMetadataParams>(
args,
isValidGetNftMetadataParams,
"Invalid NFT metadata parameters"
);
return await this.alchemy.nft.getNftMetadata(
params.contractAddress,
params.tokenId,
params
);
}
public 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);
});
```