#
tokens: 16079/50000 12/12 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
 1 | # Logs
 2 | logs
 3 | *.log
 4 | 
 5 | # Runtime data
 6 | pids
 7 | *.pid
 8 | *.seed
 9 | 
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 | 
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 | 
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 | 
19 | # node-waf configuration
20 | .lock-wscript
21 | 
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 | 
25 | # Dependency directory
26 | # https://docs.npmjs.com/cli/shrinkwrap#caveats
27 | node_modules
28 | 
29 | # Debug log from npm
30 | npm-debug.log
31 | 
32 | .DS_Store
33 | 
34 | 
35 | /node_modules
36 | 
37 | database.sqlite
38 | database.sqlite-journal
39 | 
40 | db.sqlite3
```

--------------------------------------------------------------------------------
/.clinerules:
--------------------------------------------------------------------------------

```
  1 | # MCP Plugin Development Protocol
  2 | 
  3 | ⚠️ CRITICAL: DO NOT USE attempt_completion BEFORE TESTING ⚠️
  4 | 
  5 | ## Step 1: Planning (PLAN MODE)
  6 | - What problem does this tool solve?
  7 | - What API/service will it use?
  8 | - What are the authentication requirements?
  9 |   □ Standard API key
 10 |   □ OAuth (requires separate setup script)
 11 |   □ Other credentials
 12 | 
 13 | ## Step 2: Implementation (ACT MODE)
 14 | 1. Bootstrap
 15 |    - For web services, JavaScript integration, or Node.js environments:
 16 |      ```bash
 17 |      npx @modelcontextprotocol/create-server my-server
 18 |      cd my-server
 19 |      npm install
 20 |      ```
 21 |    - For data science, ML workflows, or Python environments:
 22 |      ```bash
 23 |      pip install mcp
 24 |      # Or with uv (recommended)
 25 |      uv add "mcp[cli]"
 26 |      ```
 27 | 
 28 | 2. Core Implementation
 29 |    - Use MCP SDK
 30 |    - Implement comprehensive logging
 31 |      - TypeScript (for web/JS projects):
 32 |        ```typescript
 33 |        console.error('[Setup] Initializing server...');
 34 |        console.error('[API] Request to endpoint:', endpoint);
 35 |        console.error('[Error] Failed with:', error);
 36 |        ```
 37 |      - Python (for data science/ML projects):
 38 |        ```python
 39 |        import logging
 40 |        logging.error('[Setup] Initializing server...')
 41 |        logging.error(f'[API] Request to endpoint: {endpoint}')
 42 |        logging.error(f'[Error] Failed with: {str(error)}')
 43 |        ```
 44 |    - Add type definitions
 45 |    - Handle errors with context
 46 |    - Implement rate limiting if needed
 47 | 
 48 | 3. Configuration
 49 |    - Get credentials from user if needed
 50 |    - Add to MCP settings:
 51 |      - For TypeScript projects:
 52 |        ```json
 53 |        {
 54 |          "mcpServers": {
 55 |            "my-server": {
 56 |              "command": "node",
 57 |              "args": ["path/to/build/index.js"],
 58 |              "env": {
 59 |                "API_KEY": "key"
 60 |              },
 61 |              "disabled": false,
 62 |              "autoApprove": []
 63 |            }
 64 |          }
 65 |        }
 66 |        ```
 67 |      - For Python projects:
 68 |        ```bash
 69 |        # Directly with command line
 70 |        mcp install server.py -v API_KEY=key
 71 |        
 72 |        # Or in settings.json
 73 |        {
 74 |          "mcpServers": {
 75 |            "my-server": {
 76 |              "command": "python",
 77 |              "args": ["server.py"],
 78 |              "env": {
 79 |                "API_KEY": "key"
 80 |              },
 81 |              "disabled": false,
 82 |              "autoApprove": []
 83 |            }
 84 |          }
 85 |        }
 86 |        ```
 87 | 
 88 | ## Step 3: Testing (BLOCKER ⛔️)
 89 | 
 90 | <thinking>
 91 | BEFORE using attempt_completion, I MUST verify:
 92 | □ Have I tested EVERY tool?
 93 | □ Have I confirmed success from the user for each test?
 94 | □ Have I documented the test results?
 95 | 
 96 | If ANY answer is "no", I MUST NOT use attempt_completion.
 97 | </thinking>
 98 | 
 99 | 1. Test Each Tool (REQUIRED)
100 |    □ Test each tool with valid inputs
101 |    □ Verify output format is correct
102 |    ⚠️ DO NOT PROCEED UNTIL ALL TOOLS TESTED
103 | 
104 | ## Step 4: Completion
105 | ❗ STOP AND VERIFY:
106 | □ Every tool has been tested with valid inputs
107 | □ Output format is correct for each tool
108 | 
109 | Only after ALL tools have been tested can attempt_completion be used.
110 | 
111 | ## Key Requirements
112 | - ✓ Must use MCP SDK
113 | - ✓ Must have comprehensive logging
114 | - ✓ Must test each tool individually
115 | - ✓ Must handle errors gracefully
116 | - ⛔️ NEVER skip testing before completion
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Alchemy MCP Plugin
 2 | 
 3 | [![smithery badge](https://smithery.ai/badge/@itsanishjain/alchemy-sdk-mcp)](https://smithery.ai/server/@itsanishjain/alchemy-sdk-mcp)
 4 | 
 5 | This MCP plugin provides integration with the Alchemy SDK for blockchain and NFT operations.
 6 | 
 7 | ## Features
 8 | 
 9 | - Get NFTs for a wallet address
10 | - Get NFT metadata
11 | - Get latest block number
12 | - More endpoints can be added as needed
13 | 
14 | ## Setup
15 | 
16 | ### Installing via Smithery
17 | 
18 | To install alchemy-sdk-mcp for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@itsanishjain/alchemy-sdk-mcp):
19 | 
20 | ```bash
21 | npx -y @smithery/cli install @itsanishjain/alchemy-sdk-mcp --client claude
22 | ```
23 | 
24 | ### Manual Installation
25 | 1. Install dependencies:
26 | ```bash
27 | npm install
28 | ```
29 | 
30 | 2. Build the project:
31 | ```bash
32 | npm run build
33 | ```
34 | 
35 | 3. Configure your Alchemy API key:
36 |    - Get an API key from [Alchemy](https://www.alchemy.com/)
37 |    - Update the `ALCHEMY_API_KEY` in `settings.json`
38 | 
39 | 4. Start the server:
40 | ```bash
41 | npm start
42 | ```
43 | 
44 | ## Available Endpoints
45 | 
46 | ### 1. Get NFTs for Owner
47 | ```typescript
48 | POST /getNftsForOwner
49 | {
50 |     "owner": "wallet_address"
51 | }
52 | ```
53 | 
54 | ### 2. Get NFT Metadata
55 | ```typescript
56 | POST /getNftMetadata
57 | {
58 |     "contractAddress": "contract_address",
59 |     "tokenId": "token_id"
60 | }
61 | ```
62 | 
63 | ### 3. Get Block Number
64 | ```typescript
65 | POST /getBlockNumber
66 | ```
67 | 
68 | ## Error Handling
69 | 
70 | All endpoints include proper error handling and logging. Errors are returned in the format:
71 | ```json
72 | {
73 |     "error": "Error message"
74 | }
75 | ```
76 | 
77 | ## Logging
78 | 
79 | The server implements comprehensive logging using console.error for better debugging:
80 | - [Setup] logs for initialization
81 | - [API] logs for API calls
82 | - [Error] logs for error handling
83 | 
84 | 
85 | 
86 | $env:ALCHEMY_API_KEY="KRdhdsBezoTMVajIknIxlXgBHc1Pprpw"; node dist/index.js
87 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   "compilerOptions": {
3 |     "module": "ESNext",
4 |     "target": "ESNext",
5 |     "moduleResolution": "node",
6 |     "esModuleInterop": true
7 |   }
8 | }
```

--------------------------------------------------------------------------------
/settings.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "mcpServers": {
 3 |     "alchemy-mcp": {
 4 |       "command": "node",
 5 |       "args": ["dist/index.js"],
 6 |       "env": {
 7 |         "ALCHEMY_API_KEY": "KRdhdsBezoTMVajIknIxlXgBHc1Pprpw",
 8 |         "PORT": "3000"
 9 |       },
10 |       "disabled": false,
11 |       "autoApprove": []
12 |     }
13 |   }
14 | }
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "alchemy-mcp",
 3 |   "type": "module",
 4 |   "version": "1.0.0",
 5 |   "description": "MCP plugin for Alchemy SDK",
 6 |   "main": "dist/index.js",
 7 |   "scripts": {
 8 |     "build": "tsc",
 9 |     "start": "node dist/index.js",
10 |     "dev": "ts-node index.ts"
11 |   },
12 |   "keywords": [],
13 |   "author": "",
14 |   "license": "ISC",
15 |   "dependencies": {
16 |     "@modelcontextprotocol/sdk": "^1.6.1",
17 |     "alchemy-sdk": "^2.11.0"
18 |   },
19 |   "devDependencies": {
20 |     "@types/node": "^20.0.0",
21 |     "tsx": "^4.19.3",
22 |     "typescript": "^5.0.0"
23 |   }
24 | }
25 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM node:lts-alpine
 3 | 
 4 | # Create and set working directory
 5 | WORKDIR /app
 6 | 
 7 | # Copy package files and install dependencies
 8 | COPY package*.json ./
 9 | RUN npm install --ignore-scripts
10 | 
11 | # Copy the rest of the application
12 | COPY . .
13 | 
14 | # Build the project
15 | RUN npm run build
16 | 
17 | # Optionally expose a port if needed (the settings.json indicates port 3000, but the server communicates over stdio)
18 | # EXPOSE 3000
19 | 
20 | # Start the MCP server
21 | CMD [ "npm", "start" ]
22 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - alchemyApiKey
10 |     properties:
11 |       alchemyApiKey:
12 |         type: string
13 |         description: Your Alchemy API key for accessing the Alchemy SDK services.
14 |   commandFunction:
15 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
16 |     |-
17 |     (config) => ({
18 |       command: 'node',
19 |       args: ['dist/index.js'],
20 |       env: {
21 |         ALCHEMY_API_KEY: config.alchemyApiKey
22 |       }
23 |     })
24 |   exampleConfig:
25 |     alchemyApiKey: your_dummy_alchemy_api_key_here
26 | 
```

--------------------------------------------------------------------------------
/test-alchemy.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { Alchemy, Network } from "alchemy-sdk";
 2 | 
 3 | // Initialize Alchemy SDK with API key
 4 | const API_KEY = "KRdhdsBezoTMVajIknIxlXgBHc1Pprpw";
 5 | 
 6 | // Configure Alchemy SDK
 7 | const settings = {
 8 |   apiKey: API_KEY,
 9 |   network: Network.ETH_MAINNET,
10 | };
11 | 
12 | // Create Alchemy instance
13 | const alchemy = new Alchemy(settings);
14 | 
15 | async function testAlchemy() {
16 |   try {
17 |     console.log("Testing Alchemy API connection...");
18 | 
19 |     // Get current gas price
20 |     const gasPrice = await alchemy.core.getGasPrice();
21 |     console.log("Current gas price (wei):", gasPrice.toString());
22 |     console.log(
23 |       "Current gas price (gwei):",
24 |       parseInt(gasPrice.toString()) / 1e9
25 |     );
26 | 
27 |     // Get latest block number
28 |     const blockNumber = await alchemy.core.getBlockNumber();
29 |     console.log("Latest block number:", blockNumber);
30 | 
31 |     console.log("Alchemy API connection test successful!");
32 |   } catch (error) {
33 |     console.error("Error testing Alchemy API:", error);
34 |   }
35 | }
36 | 
37 | testAlchemy();
38 | 
```

--------------------------------------------------------------------------------
/zerops.yml:
--------------------------------------------------------------------------------

```yaml
 1 | zerops:
 2 |   - setup: alpine0
 3 |     # ==== how to build your application ====
 4 |     build:
 5 |       # what technologies should the build
 6 |       # container be based on (can be an array)
 7 |       os: alpine
 8 |       base: nodejs@20
 9 | 
10 |       # what commands to use to build your app
11 |       buildCommands:
12 |         - npm i
13 |       # select which files / folders to deploy
14 |       # after the build succesfully finished
15 |       deployFiles:
16 |         - ./
17 | 
18 |       # *optional*: which files / folders
19 |       # to cache for the next build run
20 |       cache:
21 |         - node_modules
22 |         - yarn.lock
23 | 
24 |     # ==== how to run your application ====
25 |     run:
26 |       # what technology should the runtime
27 |       # container be based on, can be extended
28 |       # in `run.prepareCommands` using
29 |       # `zsc install nodejs@20`
30 |       base: nodejs@20
31 |       os: alpine
32 |       envVariables:
33 |         PORT: "8081"
34 |       # what ports your app listens on
35 |       # and whether it supports http traffic
36 |       ports:
37 |         - port: 8081
38 |           httpSupport: true
39 | 
40 |       # how to start your application
41 |       start: npm start
42 | 
```

--------------------------------------------------------------------------------
/test-eth-price.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { Alchemy, Network, Utils } from "alchemy-sdk";
 2 | 
 3 | // Initialize Alchemy SDK with API key
 4 | const API_KEY = "KRdhdsBezoTMVajIknIxlXgBHc1Pprpw";
 5 | 
 6 | // Configure Alchemy SDK
 7 | const settings = {
 8 |   apiKey: API_KEY,
 9 |   network: Network.ETH_MAINNET,
10 | };
11 | 
12 | // Create Alchemy instance
13 | const alchemy = new Alchemy(settings);
14 | 
15 | // USDC contract address (a stable coin pegged to USD)
16 | const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
17 | 
18 | // WETH contract address (Wrapped ETH)
19 | const WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
20 | 
21 | async function getEthPrice() {
22 |   try {
23 |     console.log("Fetching ETH price...");
24 | 
25 |     // Method 1: Using Alchemy's getTokenBalances to check WETH/USDC ratio
26 |     // This is a simplified approach and not the most accurate for price data
27 |     console.log("Method 1: Using token balances (simplified approach)");
28 | 
29 |     // Get latest block number for reference
30 |     const blockNumber = await alchemy.core.getBlockNumber();
31 |     console.log("Latest block number:", blockNumber);
32 | 
33 |     // Get gas price as a basic test of Alchemy connection
34 |     const gasPrice = await alchemy.core.getGasPrice();
35 |     console.log("Current gas price (wei):", gasPrice.toString());
36 |     console.log(
37 |       "Current gas price (gwei):",
38 |       parseInt(gasPrice.toString()) / 1e9
39 |     );
40 | 
41 |     // Note: For accurate ETH price, you would typically:
42 |     // 1. Query a price oracle like Chainlink
43 |     // 2. Check a DEX like Uniswap for the ETH/USDC pair
44 |     // 3. Use a price API service
45 | 
46 |     console.log("\nFor accurate ETH price data, consider:");
47 |     console.log("1. Adding a Chainlink price feed oracle integration");
48 |     console.log("2. Querying Uniswap or another DEX for the ETH/USDC pair");
49 |     console.log("3. Using a price API service like CoinGecko or CryptoCompare");
50 | 
51 |     // Example of what the implementation might look like:
52 |     console.log("\nExample implementation (pseudocode):");
53 |     console.log(`
54 |     // Using Chainlink ETH/USD Price Feed
55 |     const ETH_USD_PRICE_FEED = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419";
56 |     const aggregatorV3InterfaceABI = [...]; // ABI for price feed
57 |     const priceFeedContract = new ethers.Contract(
58 |       ETH_USD_PRICE_FEED,
59 |       aggregatorV3InterfaceABI,
60 |       provider
61 |     );
62 |     const roundData = await priceFeedContract.latestRoundData();
63 |     const price = roundData.answer.toString() / 10**8; // Adjust for decimals
64 |     console.log("ETH price (USD):", price);
65 |     `);
66 |   } catch (error) {
67 |     console.error("Error fetching ETH price:", error);
68 |   }
69 | }
70 | 
71 | getEthPrice();
72 | 
```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
   2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
   3 | import {
   4 |   CallToolRequestSchema,
   5 |   ErrorCode,
   6 |   ListToolsRequestSchema,
   7 |   McpError,
   8 | } from "@modelcontextprotocol/sdk/types.js";
   9 | import { Alchemy, Network, Utils } from "alchemy-sdk";
  10 | 
  11 | // Initialize Alchemy SDK with API key from environment variables
  12 | const API_KEY = process.env.ALCHEMY_API_KEY;
  13 | if (!API_KEY) {
  14 |   throw new Error("ALCHEMY_API_KEY environment variable is required");
  15 | }
  16 | 
  17 | console.error("[Setup] Initializing Alchemy MCP server...");
  18 | 
  19 | // Get network from environment or default to ETH_MAINNET
  20 | const networkStr = process.env.ALCHEMY_NETWORK || "ETH_MAINNET";
  21 | const network =
  22 |   Network[networkStr as keyof typeof Network] || Network.ETH_MAINNET;
  23 | 
  24 | console.error(`[Setup] Using network: ${networkStr}`);
  25 | 
  26 | // Configure Alchemy SDK
  27 | const settings = {
  28 |   apiKey: API_KEY,
  29 |   network: network,
  30 | };
  31 | 
  32 | // Create Alchemy instance
  33 | const alchemy = new Alchemy(settings);
  34 | 
  35 | // Track active subscriptions
  36 | const activeSubscriptions: Map<string, { unsubscribe: () => void }> = new Map();
  37 | 
  38 | // Import types from alchemy-sdk
  39 | import type {
  40 |   GetNftsForOwnerOptions,
  41 |   GetNftMetadataOptions,
  42 |   AssetTransfersParams,
  43 |   GetNftSalesOptions,
  44 |   GetContractsForOwnerOptions,
  45 |   GetOwnersForNftOptions,
  46 |   GetTransfersForContractOptions,
  47 |   GetTransfersForOwnerOptions,
  48 |   TransactionReceiptsParams,
  49 |   GetTokensForOwnerOptions,
  50 |   GetBaseNftsForContractOptions,
  51 | } from "alchemy-sdk";
  52 | 
  53 | // Parameter type definitions
  54 | type GetNftsForOwnerParams = GetNftsForOwnerOptions & { owner: string };
  55 | type GetNftMetadataParams = GetNftMetadataOptions & {
  56 |   contractAddress: string;
  57 |   tokenId: string;
  58 | };
  59 | type GetTokenBalancesParams = { address: string; tokenAddresses?: string[] };
  60 | type GetAssetTransfersParams = AssetTransfersParams;
  61 | type GetNftSalesParams = GetNftSalesOptions & {
  62 |   contractAddress?: string;
  63 |   tokenId?: string;
  64 | };
  65 | type GetContractsForOwnerParams = GetContractsForOwnerOptions & {
  66 |   owner: string;
  67 | };
  68 | type GetFloorPriceParams = { contractAddress: string };
  69 | type GetOwnersForNftParams = GetOwnersForNftOptions & {
  70 |   contractAddress: string;
  71 |   tokenId: string;
  72 | };
  73 | type GetTransfersForContractParams = GetTransfersForContractOptions & {
  74 |   contractAddress: string;
  75 | };
  76 | type GetTransfersForOwnerParams = GetTransfersForOwnerOptions & {
  77 |   owner: string;
  78 | };
  79 | type GetTransactionReceiptsParams = TransactionReceiptsParams;
  80 | type GetTokenMetadataParams = { contractAddress: string };
  81 | type GetTokensForOwnerParams = GetTokensForOwnerOptions & { owner: string };
  82 | type GetNftsForContractParams = GetBaseNftsForContractOptions & {
  83 |   contractAddress: string;
  84 | };
  85 | type GetBlockWithTransactionsParams = {
  86 |   blockNumber?: string | number;
  87 |   blockHash?: string;
  88 | };
  89 | type GetTransactionParams = { hash: string };
  90 | type ResolveEnsParams = { name: string; blockTag?: string | number };
  91 | type LookupAddressParams = { address: string };
  92 | type EstimateGasPriceParams = { maxFeePerGas?: boolean };
  93 | type SubscribeParams = {
  94 |   type: string;
  95 |   address?: string;
  96 |   topics?: string[];
  97 | };
  98 | type UnsubscribeParams = {
  99 |   subscriptionId: string;
 100 | };
 101 | 
 102 | // Validation functions (keeping them as they were, just showing a few as example)
 103 | const isValidGetNftsForOwnerParams = (
 104 |   args: any
 105 | ): args is GetNftsForOwnerParams => {
 106 |   return (
 107 |     typeof args === "object" &&
 108 |     args !== null &&
 109 |     typeof args.owner === "string" &&
 110 |     (args.pageKey === undefined || typeof args.pageKey === "string") &&
 111 |     (args.pageSize === undefined || typeof args.pageSize === "number") &&
 112 |     (args.contractAddresses === undefined ||
 113 |       Array.isArray(args.contractAddresses)) &&
 114 |     (args.withMetadata === undefined || typeof args.withMetadata === "boolean")
 115 |   );
 116 | };
 117 | 
 118 | const isValidGetNftMetadataParams = (
 119 |   args: any
 120 | ): args is GetNftMetadataParams => {
 121 |   return (
 122 |     typeof args === "object" &&
 123 |     args !== null &&
 124 |     typeof args.contractAddress === "string" &&
 125 |     typeof args.tokenId === "string" &&
 126 |     (args.tokenType === undefined || typeof args.tokenType === "string") &&
 127 |     (args.refreshCache === undefined || typeof args.refreshCache === "boolean")
 128 |   );
 129 | };
 130 | 
 131 | const isValidGetTokenBalancesParams = (
 132 |   args: any
 133 | ): args is GetTokenBalancesParams => {
 134 |   return (
 135 |     typeof args === "object" &&
 136 |     args !== null &&
 137 |     typeof args.address === "string" &&
 138 |     (args.tokenAddresses === undefined || Array.isArray(args.tokenAddresses))
 139 |   );
 140 | };
 141 | 
 142 | const isValidGetAssetTransfersParams = (
 143 |   args: any
 144 | ): args is GetAssetTransfersParams => {
 145 |   return (
 146 |     typeof args === "object" &&
 147 |     args !== null &&
 148 |     (args.fromBlock === undefined || typeof args.fromBlock === "string") &&
 149 |     (args.toBlock === undefined || typeof args.toBlock === "string") &&
 150 |     (args.fromAddress === undefined || typeof args.fromAddress === "string") &&
 151 |     (args.toAddress === undefined || typeof args.toAddress === "string") &&
 152 |     (args.category === undefined || Array.isArray(args.category)) &&
 153 |     (args.contractAddresses === undefined ||
 154 |       Array.isArray(args.contractAddresses)) &&
 155 |     (args.maxCount === undefined || typeof args.maxCount === "number") &&
 156 |     (args.excludeZeroValue === undefined ||
 157 |       typeof args.excludeZeroValue === "boolean") &&
 158 |     (args.pageKey === undefined || typeof args.pageKey === "string") &&
 159 |     (args.withMetadata === undefined || typeof args.withMetadata === "boolean")
 160 |   );
 161 | };
 162 | 
 163 | const isValidGetNftSalesParams = (args: any): args is GetNftSalesParams => {
 164 |   return (
 165 |     typeof args === "object" &&
 166 |     args !== null &&
 167 |     (args.contractAddress === undefined ||
 168 |       typeof args.contractAddress === "string") &&
 169 |     (args.tokenId === undefined || typeof args.tokenId === "string") &&
 170 |     (args.fromBlock === undefined || typeof args.fromBlock === "number") &&
 171 |     (args.toBlock === undefined || typeof args.toBlock === "number") &&
 172 |     (args.order === undefined || typeof args.order === "string") &&
 173 |     (args.marketplace === undefined || typeof args.marketplace === "string") &&
 174 |     (args.pageKey === undefined || typeof args.pageKey === "string") &&
 175 |     (args.pageSize === undefined || typeof args.pageSize === "number")
 176 |   );
 177 | };
 178 | 
 179 | const isValidGetContractsForOwnerParams = (
 180 |   args: any
 181 | ): args is GetContractsForOwnerParams => {
 182 |   return (
 183 |     typeof args === "object" &&
 184 |     args !== null &&
 185 |     typeof args.owner === "string" &&
 186 |     (args.pageKey === undefined || typeof args.pageKey === "string") &&
 187 |     (args.pageSize === undefined || typeof args.pageSize === "number") &&
 188 |     (args.includeFilters === undefined || Array.isArray(args.includeFilters)) &&
 189 |     (args.excludeFilters === undefined || Array.isArray(args.excludeFilters))
 190 |   );
 191 | };
 192 | 
 193 | const isValidGetFloorPriceParams = (args: any): args is GetFloorPriceParams => {
 194 |   return (
 195 |     typeof args === "object" &&
 196 |     args !== null &&
 197 |     typeof args.contractAddress === "string"
 198 |   );
 199 | };
 200 | 
 201 | const isValidGetOwnersForNftParams = (
 202 |   args: any
 203 | ): args is GetOwnersForNftParams => {
 204 |   return (
 205 |     typeof args === "object" &&
 206 |     args !== null &&
 207 |     typeof args.contractAddress === "string" &&
 208 |     typeof args.tokenId === "string" &&
 209 |     (args.pageKey === undefined || typeof args.pageKey === "string") &&
 210 |     (args.pageSize === undefined || typeof args.pageSize === "number")
 211 |   );
 212 | };
 213 | 
 214 | const isValidGetTransfersForContractParams = (
 215 |   args: any
 216 | ): args is GetTransfersForContractParams => {
 217 |   return (
 218 |     typeof args === "object" &&
 219 |     args !== null &&
 220 |     typeof args.contractAddress === "string" &&
 221 |     (args.pageKey === undefined || typeof args.pageKey === "string") &&
 222 |     (args.fromBlock === undefined || typeof args.fromBlock === "number") &&
 223 |     (args.toBlock === undefined || typeof args.toBlock === "number") &&
 224 |     (args.order === undefined || typeof args.order === "string") &&
 225 |     (args.tokenType === undefined || typeof args.tokenType === "string")
 226 |   );
 227 | };
 228 | 
 229 | const isValidGetTransfersForOwnerParams = (
 230 |   args: any
 231 | ): args is GetTransfersForOwnerParams => {
 232 |   return (
 233 |     typeof args === "object" &&
 234 |     args !== null &&
 235 |     typeof args.owner === "string" &&
 236 |     (args.pageKey === undefined || typeof args.pageKey === "string") &&
 237 |     (args.fromBlock === undefined || typeof args.fromBlock === "number") &&
 238 |     (args.toBlock === undefined || typeof args.toBlock === "number") &&
 239 |     (args.order === undefined || typeof args.order === "string") &&
 240 |     (args.tokenType === undefined || typeof args.tokenType === "string") &&
 241 |     (args.contractAddresses === undefined ||
 242 |       Array.isArray(args.contractAddresses))
 243 |   );
 244 | };
 245 | 
 246 | const isValidGetTransactionReceiptsParams = (
 247 |   args: any
 248 | ): args is GetTransactionReceiptsParams => {
 249 |   return (
 250 |     typeof args === "object" &&
 251 |     args !== null &&
 252 |     (args.blockHash !== undefined || args.blockNumber !== undefined) &&
 253 |     (args.blockHash === undefined || typeof args.blockHash === "string") &&
 254 |     (args.blockNumber === undefined ||
 255 |       typeof args.blockNumber === "string" ||
 256 |       typeof args.blockNumber === "number")
 257 |   );
 258 | };
 259 | 
 260 | const isValidGetTokenMetadataParams = (
 261 |   args: any
 262 | ): args is GetTokenMetadataParams => {
 263 |   return (
 264 |     typeof args === "object" &&
 265 |     args !== null &&
 266 |     typeof args.contractAddress === "string"
 267 |   );
 268 | };
 269 | 
 270 | const isValidGetTokensForOwnerParams = (
 271 |   args: any
 272 | ): args is GetTokensForOwnerParams => {
 273 |   return (
 274 |     typeof args === "object" &&
 275 |     args !== null &&
 276 |     typeof args.owner === "string" &&
 277 |     (args.pageKey === undefined || typeof args.pageKey === "string") &&
 278 |     (args.pageSize === undefined || typeof args.pageSize === "number") &&
 279 |     (args.contractAddresses === undefined ||
 280 |       Array.isArray(args.contractAddresses))
 281 |   );
 282 | };
 283 | 
 284 | const isValidGetNftsForContractParams = (
 285 |   args: any
 286 | ): args is GetNftsForContractParams => {
 287 |   return (
 288 |     typeof args === "object" &&
 289 |     args !== null &&
 290 |     typeof args.contractAddress === "string" &&
 291 |     (args.pageKey === undefined || typeof args.pageKey === "string") &&
 292 |     (args.pageSize === undefined || typeof args.pageSize === "number") &&
 293 |     (args.tokenUriTimeoutInMs === undefined ||
 294 |       typeof args.tokenUriTimeoutInMs === "number") &&
 295 |     (args.withMetadata === undefined || typeof args.withMetadata === "boolean")
 296 |   );
 297 | };
 298 | 
 299 | const isValidGetBlockWithTransactionsParams = (
 300 |   args: any
 301 | ): args is GetBlockWithTransactionsParams => {
 302 |   return (
 303 |     typeof args === "object" &&
 304 |     args !== null &&
 305 |     (args.blockNumber !== undefined || args.blockHash !== undefined) &&
 306 |     (args.blockNumber === undefined ||
 307 |       typeof args.blockNumber === "string" ||
 308 |       typeof args.blockNumber === "number") &&
 309 |     (args.blockHash === undefined || typeof args.blockHash === "string")
 310 |   );
 311 | };
 312 | 
 313 | const isValidGetTransactionParams = (
 314 |   args: any
 315 | ): args is GetTransactionParams => {
 316 |   return (
 317 |     typeof args === "object" && args !== null && typeof args.hash === "string"
 318 |   );
 319 | };
 320 | 
 321 | const isValidResolveEnsParams = (args: any): args is ResolveEnsParams => {
 322 |   return (
 323 |     typeof args === "object" &&
 324 |     args !== null &&
 325 |     typeof args.name === "string" &&
 326 |     (args.blockTag === undefined ||
 327 |       typeof args.blockTag === "string" ||
 328 |       typeof args.blockTag === "number")
 329 |   );
 330 | };
 331 | 
 332 | const isValidLookupAddressParams = (args: any): args is LookupAddressParams => {
 333 |   return (
 334 |     typeof args === "object" &&
 335 |     args !== null &&
 336 |     typeof args.address === "string"
 337 |   );
 338 | };
 339 | 
 340 | const isValidEstimateGasPriceParams = (
 341 |   args: any
 342 | ): args is EstimateGasPriceParams => {
 343 |   return (
 344 |     typeof args === "object" &&
 345 |     args !== null &&
 346 |     (args.maxFeePerGas === undefined || typeof args.maxFeePerGas === "boolean")
 347 |   );
 348 | };
 349 | 
 350 | const isValidSubscribeParams = (args: any): args is SubscribeParams => {
 351 |   return (
 352 |     typeof args === "object" &&
 353 |     args !== null &&
 354 |     typeof args.type === "string" &&
 355 |     (args.address === undefined || typeof args.address === "string") &&
 356 |     (args.topics === undefined || Array.isArray(args.topics))
 357 |   );
 358 | };
 359 | 
 360 | const isValidUnsubscribeParams = (args: any): args is UnsubscribeParams => {
 361 |   return (
 362 |     typeof args === "object" &&
 363 |     args !== null &&
 364 |     typeof args.subscriptionId === "string"
 365 |   );
 366 | };
 367 | 
 368 | export class AlchemyMcpServer {
 369 |   private server: Server;
 370 |   private alchemy: Alchemy;
 371 |   private activeSubscriptions: Map<string, { unsubscribe: () => void }>;
 372 | 
 373 |   constructor() {
 374 |     this.server = new Server(
 375 |       {
 376 |         name: "alchemy-sdk-server",
 377 |         version: "1.0.0",
 378 |       },
 379 |       {
 380 |         capabilities: {
 381 |           tools: {},
 382 |         },
 383 |       }
 384 |     );
 385 | 
 386 |     this.alchemy = alchemy;
 387 |     this.activeSubscriptions = activeSubscriptions;
 388 | 
 389 |     this.setupToolHandlers();
 390 | 
 391 |     this.server.onerror = (error) => console.error("[MCP Error]", error);
 392 | 
 393 |     process.on("SIGINT", async () => {
 394 |       for (const [id, subscription] of this.activeSubscriptions.entries()) {
 395 |         try {
 396 |           subscription.unsubscribe();
 397 |           console.error(`[Cleanup] Unsubscribed from subscription ${id}`);
 398 |         } catch (error) {
 399 |           console.error(`[Cleanup] Failed to unsubscribe from ${id}:`, error);
 400 |         }
 401 |       }
 402 |       await this.server.close();
 403 |       process.exit(0);
 404 |     });
 405 |   }
 406 | 
 407 |   private setupToolHandlers() {
 408 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
 409 |       tools: [
 410 |         // NFT API Tools
 411 |         {
 412 |           name: "get_nfts_for_owner",
 413 |           description: "Get NFTs owned by a specific wallet address",
 414 |           inputSchema: {
 415 |             type: "object",
 416 |             properties: {
 417 |               owner: {
 418 |                 type: "string",
 419 |                 description: "The wallet address to get NFTs for",
 420 |               },
 421 |               pageKey: {
 422 |                 type: "string",
 423 |                 description: "Key for pagination",
 424 |               },
 425 |               pageSize: {
 426 |                 type: "number",
 427 |                 description: "Number of NFTs to return in one page (max: 100)",
 428 |               },
 429 |               contractAddresses: {
 430 |                 type: "array",
 431 |                 items: {
 432 |                   type: "string",
 433 |                 },
 434 |                 description: "List of contract addresses to filter by",
 435 |               },
 436 |               withMetadata: {
 437 |                 type: "boolean",
 438 |                 description: "Whether to include NFT metadata",
 439 |               },
 440 |             },
 441 |             required: ["owner"],
 442 |           },
 443 |         },
 444 |         {
 445 |           name: "get_nft_metadata",
 446 |           description: "Get metadata for a specific NFT",
 447 |           inputSchema: {
 448 |             type: "object",
 449 |             properties: {
 450 |               contractAddress: {
 451 |                 type: "string",
 452 |                 description: "The contract address of the NFT",
 453 |               },
 454 |               tokenId: {
 455 |                 type: "string",
 456 |                 description: "The token ID of the NFT",
 457 |               },
 458 |               tokenType: {
 459 |                 type: "string",
 460 |                 description: "The token type (ERC721 or ERC1155)",
 461 |               },
 462 |               refreshCache: {
 463 |                 type: "boolean",
 464 |                 description: "Whether to refresh the cache",
 465 |               },
 466 |             },
 467 |             required: ["contractAddress", "tokenId"],
 468 |           },
 469 |         },
 470 |         {
 471 |           name: "get_nft_sales",
 472 |           description: "Get NFT sales data for a contract or specific NFT",
 473 |           inputSchema: {
 474 |             type: "object",
 475 |             properties: {
 476 |               contractAddress: {
 477 |                 type: "string",
 478 |                 description: "The contract address of the NFT collection",
 479 |               },
 480 |               tokenId: {
 481 |                 type: "string",
 482 |                 description: "The token ID of the specific NFT",
 483 |               },
 484 |               fromBlock: {
 485 |                 type: "number",
 486 |                 description: "Starting block number for the query",
 487 |               },
 488 |               toBlock: {
 489 |                 type: "number",
 490 |                 description: "Ending block number for the query",
 491 |               },
 492 |               order: {
 493 |                 type: "string",
 494 |                 enum: ["asc", "desc"],
 495 |                 description: "Order of results (ascending or descending)",
 496 |               },
 497 |               marketplace: {
 498 |                 type: "string",
 499 |                 description:
 500 |                   "Filter by marketplace (e.g., 'seaport', 'wyvern')",
 501 |               },
 502 |               pageKey: {
 503 |                 type: "string",
 504 |                 description: "Key for pagination",
 505 |               },
 506 |               pageSize: {
 507 |                 type: "number",
 508 |                 description: "Number of results per page",
 509 |               },
 510 |             },
 511 |           },
 512 |         },
 513 |         {
 514 |           name: "get_contracts_for_owner",
 515 |           description: "Get NFT contracts owned by an address",
 516 |           inputSchema: {
 517 |             type: "object",
 518 |             properties: {
 519 |               owner: {
 520 |                 type: "string",
 521 |                 description: "The wallet address to get contracts for",
 522 |               },
 523 |               pageKey: {
 524 |                 type: "string",
 525 |                 description: "Key for pagination",
 526 |               },
 527 |               pageSize: {
 528 |                 type: "number",
 529 |                 description: "Number of results per page",
 530 |               },
 531 |               includeFilters: {
 532 |                 type: "array",
 533 |                 items: {
 534 |                   type: "string",
 535 |                   enum: ["spam", "airdrops"],
 536 |                 },
 537 |                 description: "Filters to include in the response",
 538 |               },
 539 |               excludeFilters: {
 540 |                 type: "array",
 541 |                 items: {
 542 |                   type: "string",
 543 |                   enum: ["spam", "airdrops"],
 544 |                 },
 545 |                 description: "Filters to exclude from the response",
 546 |               },
 547 |             },
 548 |             required: ["owner"],
 549 |           },
 550 |         },
 551 |         {
 552 |           name: "get_floor_price",
 553 |           description: "Get floor price for an NFT collection",
 554 |           inputSchema: {
 555 |             type: "object",
 556 |             properties: {
 557 |               contractAddress: {
 558 |                 type: "string",
 559 |                 description: "The contract address of the NFT collection",
 560 |               },
 561 |             },
 562 |             required: ["contractAddress"],
 563 |           },
 564 |         },
 565 |         {
 566 |           name: "get_owners_for_nft",
 567 |           description: "Get owners of a specific NFT",
 568 |           inputSchema: {
 569 |             type: "object",
 570 |             properties: {
 571 |               contractAddress: {
 572 |                 type: "string",
 573 |                 description: "The contract address of the NFT",
 574 |               },
 575 |               tokenId: {
 576 |                 type: "string",
 577 |                 description: "The token ID of the NFT",
 578 |               },
 579 |               pageKey: {
 580 |                 type: "string",
 581 |                 description: "Key for pagination",
 582 |               },
 583 |               pageSize: {
 584 |                 type: "number",
 585 |                 description: "Number of results per page",
 586 |               },
 587 |             },
 588 |             required: ["contractAddress", "tokenId"],
 589 |           },
 590 |         },
 591 |         {
 592 |           name: "get_nfts_for_contract",
 593 |           description: "Get all NFTs for a contract",
 594 |           inputSchema: {
 595 |             type: "object",
 596 |             properties: {
 597 |               contractAddress: {
 598 |                 type: "string",
 599 |                 description: "The contract address of the NFT collection",
 600 |               },
 601 |               pageKey: {
 602 |                 type: "string",
 603 |                 description: "Key for pagination",
 604 |               },
 605 |               pageSize: {
 606 |                 type: "number",
 607 |                 description: "Number of results per page",
 608 |               },
 609 |               tokenUriTimeoutInMs: {
 610 |                 type: "number",
 611 |                 description: "Timeout for token URI resolution in milliseconds",
 612 |               },
 613 |               withMetadata: {
 614 |                 type: "boolean",
 615 |                 description: "Whether to include metadata",
 616 |               },
 617 |             },
 618 |             required: ["contractAddress"],
 619 |           },
 620 |         },
 621 |         {
 622 |           name: "get_transfers_for_contract",
 623 |           description: "Get transfers for an NFT contract",
 624 |           inputSchema: {
 625 |             type: "object",
 626 |             properties: {
 627 |               contractAddress: {
 628 |                 type: "string",
 629 |                 description: "The contract address of the NFT collection",
 630 |               },
 631 |               pageKey: {
 632 |                 type: "string",
 633 |                 description: "Key for pagination",
 634 |               },
 635 |               fromBlock: {
 636 |                 type: "number",
 637 |                 description: "Starting block number for the query",
 638 |               },
 639 |               toBlock: {
 640 |                 type: "number",
 641 |                 description: "Ending block number for the query",
 642 |               },
 643 |               order: {
 644 |                 type: "string",
 645 |                 enum: ["asc", "desc"],
 646 |                 description: "Order of results (ascending or descending)",
 647 |               },
 648 |               tokenType: {
 649 |                 type: "string",
 650 |                 enum: ["ERC721", "ERC1155"],
 651 |                 description: "Type of token (ERC721 or ERC1155)",
 652 |               },
 653 |             },
 654 |             required: ["contractAddress"],
 655 |           },
 656 |         },
 657 |         {
 658 |           name: "get_transfers_for_owner",
 659 |           description: "Get NFT transfers for an owner",
 660 |           inputSchema: {
 661 |             type: "object",
 662 |             properties: {
 663 |               owner: {
 664 |                 type: "string",
 665 |                 description: "The wallet address to get transfers for",
 666 |               },
 667 |               pageKey: {
 668 |                 type: "string",
 669 |                 description: "Key for pagination",
 670 |               },
 671 |               fromBlock: {
 672 |                 type: "number",
 673 |                 description: "Starting block number for the query",
 674 |               },
 675 |               toBlock: {
 676 |                 type: "number",
 677 |                 description: "Ending block number for the query",
 678 |               },
 679 |               order: {
 680 |                 type: "string",
 681 |                 enum: ["asc", "desc"],
 682 |                 description: "Order of results (ascending or descending)",
 683 |               },
 684 |               tokenType: {
 685 |                 type: "string",
 686 |                 enum: ["ERC721", "ERC1155"],
 687 |                 description: "Type of token (ERC721 or ERC1155)",
 688 |               },
 689 |               contractAddresses: {
 690 |                 type: "array",
 691 |                 items: {
 692 |                   type: "string",
 693 |                 },
 694 |                 description: "List of contract addresses to filter by",
 695 |               },
 696 |             },
 697 |             required: ["owner"],
 698 |           },
 699 |         },
 700 | 
 701 |         // Core API Tools
 702 |         {
 703 |           name: "get_token_balances",
 704 |           description: "Get token balances for a specific address",
 705 |           inputSchema: {
 706 |             type: "object",
 707 |             properties: {
 708 |               address: {
 709 |                 type: "string",
 710 |                 description: "The wallet address to get token balances for",
 711 |               },
 712 |               tokenAddresses: {
 713 |                 type: "array",
 714 |                 items: {
 715 |                   type: "string",
 716 |                 },
 717 |                 description: "List of token addresses to filter by",
 718 |               },
 719 |             },
 720 |             required: ["address"],
 721 |           },
 722 |         },
 723 |         {
 724 |           name: "get_token_metadata",
 725 |           description: "Get metadata for a token contract",
 726 |           inputSchema: {
 727 |             type: "object",
 728 |             properties: {
 729 |               contractAddress: {
 730 |                 type: "string",
 731 |                 description: "The contract address of the token",
 732 |               },
 733 |             },
 734 |             required: ["contractAddress"],
 735 |           },
 736 |         },
 737 |         {
 738 |           name: "get_tokens_for_owner",
 739 |           description: "Get tokens owned by an address",
 740 |           inputSchema: {
 741 |             type: "object",
 742 |             properties: {
 743 |               owner: {
 744 |                 type: "string",
 745 |                 description: "The wallet address to get tokens for",
 746 |               },
 747 |               pageKey: {
 748 |                 type: "string",
 749 |                 description: "Key for pagination",
 750 |               },
 751 |               pageSize: {
 752 |                 type: "number",
 753 |                 description: "Number of results per page",
 754 |               },
 755 |               contractAddresses: {
 756 |                 type: "array",
 757 |                 items: {
 758 |                   type: "string",
 759 |                 },
 760 |                 description: "List of contract addresses to filter by",
 761 |               },
 762 |             },
 763 |             required: ["owner"],
 764 |           },
 765 |         },
 766 |         {
 767 |           name: "get_asset_transfers",
 768 |           description: "Get asset transfers for a specific address or contract",
 769 |           inputSchema: {
 770 |             type: "object",
 771 |             properties: {
 772 |               fromBlock: {
 773 |                 type: "string",
 774 |                 description: 'The starting block (hex string or "latest")',
 775 |               },
 776 |               toBlock: {
 777 |                 type: "string",
 778 |                 description: 'The ending block (hex string or "latest")',
 779 |               },
 780 |               fromAddress: {
 781 |                 type: "string",
 782 |                 description: "The sender address",
 783 |               },
 784 |               toAddress: {
 785 |                 type: "string",
 786 |                 description: "The recipient address",
 787 |               },
 788 |               category: {
 789 |                 type: "array",
 790 |                 items: {
 791 |                   type: "string",
 792 |                   enum: [
 793 |                     "external",
 794 |                     "internal",
 795 |                     "erc20",
 796 |                     "erc721",
 797 |                     "erc1155",
 798 |                     "specialnft",
 799 |                   ],
 800 |                 },
 801 |                 description:
 802 |                   'The category of transfers to include (e.g., "external", "internal", "erc20", "erc721", "erc1155", "specialnft")',
 803 |               },
 804 |               contractAddresses: {
 805 |                 type: "array",
 806 |                 items: {
 807 |                   type: "string",
 808 |                 },
 809 |                 description: "List of contract addresses to filter by",
 810 |               },
 811 |               maxCount: {
 812 |                 type: "number",
 813 |                 description: "The maximum number of results to return",
 814 |               },
 815 |               excludeZeroValue: {
 816 |                 type: "boolean",
 817 |                 description: "Whether to exclude zero value transfers",
 818 |               },
 819 |               pageKey: {
 820 |                 type: "string",
 821 |                 description: "Key for pagination",
 822 |               },
 823 |               withMetadata: {
 824 |                 type: "boolean",
 825 |                 description: "Whether to include metadata in the response",
 826 |               },
 827 |             },
 828 |           },
 829 |         },
 830 |         {
 831 |           name: "get_transaction_receipts",
 832 |           description: "Get transaction receipts for a block",
 833 |           inputSchema: {
 834 |             type: "object",
 835 |             properties: {
 836 |               blockHash: {
 837 |                 type: "string",
 838 |                 description: "The hash of the block",
 839 |               },
 840 |               blockNumber: {
 841 |                 type: "string",
 842 |                 description: "The number of the block",
 843 |               },
 844 |             },
 845 |             oneOf: [{ required: ["blockHash"] }, { required: ["blockNumber"] }],
 846 |           },
 847 |         },
 848 |         {
 849 |           name: "get_block_number",
 850 |           description: "Get the latest block number",
 851 |           inputSchema: {
 852 |             type: "object",
 853 |             properties: {},
 854 |           },
 855 |         },
 856 |         {
 857 |           name: "get_block_with_transactions",
 858 |           description: "Get a block with its transactions",
 859 |           inputSchema: {
 860 |             type: "object",
 861 |             properties: {
 862 |               blockNumber: {
 863 |                 type: "string",
 864 |                 description: "The block number",
 865 |               },
 866 |               blockHash: {
 867 |                 type: "string",
 868 |                 description: "The block hash",
 869 |               },
 870 |             },
 871 |             oneOf: [{ required: ["blockNumber"] }, { required: ["blockHash"] }],
 872 |           },
 873 |         },
 874 |         {
 875 |           name: "get_transaction",
 876 |           description: "Get transaction details by hash",
 877 |           inputSchema: {
 878 |             type: "object",
 879 |             properties: {
 880 |               hash: {
 881 |                 type: "string",
 882 |                 description: "The transaction hash",
 883 |               },
 884 |             },
 885 |             required: ["hash"],
 886 |           },
 887 |         },
 888 |         {
 889 |           name: "resolve_ens",
 890 |           description: "Resolve an ENS name to an address",
 891 |           inputSchema: {
 892 |             type: "object",
 893 |             properties: {
 894 |               name: {
 895 |                 type: "string",
 896 |                 description: "The ENS name to resolve",
 897 |               },
 898 |               blockTag: {
 899 |                 type: "string",
 900 |                 description: "The block tag to use for resolution",
 901 |               },
 902 |             },
 903 |             required: ["name"],
 904 |           },
 905 |         },
 906 |         {
 907 |           name: "lookup_address",
 908 |           description: "Lookup the ENS name for an address",
 909 |           inputSchema: {
 910 |             type: "object",
 911 |             properties: {
 912 |               address: {
 913 |                 type: "string",
 914 |                 description: "The address to lookup",
 915 |               },
 916 |             },
 917 |             required: ["address"],
 918 |           },
 919 |         },
 920 |         {
 921 |           name: "estimate_gas_price",
 922 |           description: "Estimate current gas price",
 923 |           inputSchema: {
 924 |             type: "object",
 925 |             properties: {
 926 |               maxFeePerGas: {
 927 |                 type: "boolean",
 928 |                 description:
 929 |                   "Whether to include maxFeePerGas and maxPriorityFeePerGas",
 930 |               },
 931 |             },
 932 |           },
 933 |         },
 934 | 
 935 |         // WebSocket Subscription Tools
 936 |         {
 937 |           name: "subscribe",
 938 |           description: "Subscribe to blockchain events",
 939 |           inputSchema: {
 940 |             type: "object",
 941 |             properties: {
 942 |               type: {
 943 |                 type: "string",
 944 |                 enum: ["newHeads", "logs", "pendingTransactions", "mined"],
 945 |                 description: "The type of subscription",
 946 |               },
 947 |               address: {
 948 |                 type: "string",
 949 |                 description: "The address to filter by (for logs)",
 950 |               },
 951 |               topics: {
 952 |                 type: "array",
 953 |                 items: {
 954 |                   type: "string",
 955 |                 },
 956 |                 description: "The topics to filter by (for logs)",
 957 |               },
 958 |             },
 959 |             required: ["type"],
 960 |           },
 961 |         },
 962 |         {
 963 |           name: "unsubscribe",
 964 |           description: "Unsubscribe from blockchain events",
 965 |           inputSchema: {
 966 |             type: "object",
 967 |             properties: {
 968 |               subscriptionId: {
 969 |                 type: "string",
 970 |                 description: "The ID of the subscription to cancel",
 971 |               },
 972 |             },
 973 |             required: ["subscriptionId"],
 974 |           },
 975 |         },
 976 |       ],
 977 |     }));
 978 | 
 979 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
 980 |       try {
 981 |         if (!request.params.arguments) {
 982 |           throw new McpError(ErrorCode.InvalidParams, "Missing arguments");
 983 |         }
 984 | 
 985 |         let result: unknown;
 986 |         switch (request.params.name) {
 987 |           case "get_nfts_for_owner":
 988 |             result = await this.handleGetNftsForOwner(request.params.arguments);
 989 |             break;
 990 |           case "get_nft_metadata":
 991 |             result = await this.handleGetNftMetadata(request.params.arguments);
 992 |             break;
 993 |           // ... (other cases remain the same)
 994 |           case "estimate_gas_price":
 995 |             result = await this.handleEstimateGasPrice(
 996 |               request.params.arguments
 997 |             );
 998 |             break;
 999 |           case "subscribe":
1000 |             result = await this.handleSubscribe(request.params.arguments);
1001 |             break;
1002 |           case "unsubscribe":
1003 |             result = await this.handleUnsubscribe(request.params.arguments);
1004 |             break;
1005 |           default:
1006 |             throw new McpError(
1007 |               ErrorCode.InvalidParams,
1008 |               `Unknown tool: ${request.params.name}`
1009 |             );
1010 |         }
1011 | 
1012 |         return {
1013 |           content: [
1014 |             {
1015 |               type: "text",
1016 |               text: JSON.stringify(result),
1017 |             },
1018 |           ],
1019 |         };
1020 |       } catch (error) {
1021 |         console.error("[Tool Error]", error);
1022 |         throw new McpError(
1023 |           ErrorCode.InternalError,
1024 |           `Tool error: ${
1025 |             error instanceof Error ? error.message : String(error)
1026 |           }`
1027 |         );
1028 |       }
1029 |     });
1030 |   }
1031 | 
1032 |   private validateAndCastParams<T>(
1033 |     args: Record<string, unknown>,
1034 |     validator: (args: any) => boolean,
1035 |     errorMessage: string
1036 |   ): T {
1037 |     if (!validator(args)) {
1038 |       throw new McpError(ErrorCode.InvalidParams, errorMessage);
1039 |     }
1040 |     return args as T;
1041 |   }
1042 | 
1043 |   isValidEstimateGasPriceParams = (
1044 |     args: any
1045 |   ): args is EstimateGasPriceParams => {
1046 |     return (
1047 |       typeof args === "object" &&
1048 |       args !== null &&
1049 |       (args.maxFeePerGas === undefined ||
1050 |         typeof args.maxFeePerGas === "boolean")
1051 |     );
1052 |   };
1053 | 
1054 |   isValidSubscribeParams = (args: any): args is SubscribeParams => {
1055 |     return (
1056 |       typeof args === "object" &&
1057 |       args !== null &&
1058 |       typeof args.type === "string" &&
1059 |       ["newHeads", "logs", "pendingTransactions", "mined"].includes(
1060 |         args.type
1061 |       ) &&
1062 |       (args.address === undefined || typeof args.address === "string") &&
1063 |       (args.topics === undefined || Array.isArray(args.topics))
1064 |     );
1065 |   };
1066 | 
1067 |   isValidUnsubscribeParams = (args: any): args is UnsubscribeParams => {
1068 |     return (
1069 |       typeof args === "object" &&
1070 |       args !== null &&
1071 |       typeof args.subscriptionId === "string"
1072 |     );
1073 |   };
1074 | 
1075 |   // Then in your AlchemyMcpServer class, make sure these handlers are included:
1076 | 
1077 |   private async handleEstimateGasPrice(args: Record<string, unknown>) {
1078 |     const params = this.validateAndCastParams<EstimateGasPriceParams>(
1079 |       args,
1080 |       isValidEstimateGasPriceParams,
1081 |       "Invalid gas price parameters"
1082 |     );
1083 |     const gasPrice = await this.alchemy.core.getGasPrice();
1084 |     return params.maxFeePerGas
1085 |       ? { gasPrice: Utils.formatUnits(gasPrice, "gwei") }
1086 |       : { gasPrice };
1087 |   }
1088 | 
1089 |   private async handleSubscribe(args: Record<string, unknown>) {
1090 |     const params = this.validateAndCastParams<SubscribeParams>(
1091 |       args,
1092 |       isValidSubscribeParams,
1093 |       "Invalid subscribe parameters"
1094 |     );
1095 | 
1096 |     const subscriptionId = Math.random().toString(36).substring(7);
1097 |     let subscription;
1098 | 
1099 |     switch (params.type) {
1100 |       case "newHeads":
1101 |         subscription = this.alchemy.ws.on("block", (blockNumber) => {
1102 |           console.log("[WebSocket] New block:", blockNumber);
1103 |         });
1104 |         break;
1105 |       case "logs":
1106 |         subscription = this.alchemy.ws.on(
1107 |           {
1108 |             address: params.address,
1109 |             topics: params.topics,
1110 |           },
1111 |           (log) => {
1112 |             console.log("[WebSocket] New log:", log);
1113 |           }
1114 |         );
1115 |         break;
1116 |       case "pendingTransactions":
1117 |         subscription = this.alchemy.ws.on("pending", (tx) => {
1118 |           console.log("[WebSocket] Pending transaction:", tx);
1119 |         });
1120 |         break;
1121 |       case "mined":
1122 |         subscription = this.alchemy.ws.on("mined", (tx) => {
1123 |           console.log("[WebSocket] Mined transaction:", tx);
1124 |         });
1125 |         break;
1126 |       default:
1127 |         throw new McpError(
1128 |           ErrorCode.InvalidParams,
1129 |           `Unknown subscription type: ${params.type}`
1130 |         );
1131 |     }
1132 | 
1133 |     this.activeSubscriptions.set(subscriptionId, subscription);
1134 |     return { subscriptionId };
1135 |   }
1136 | 
1137 |   private async handleUnsubscribe(args: Record<string, unknown>) {
1138 |     const params = this.validateAndCastParams<UnsubscribeParams>(
1139 |       args,
1140 |       isValidUnsubscribeParams,
1141 |       "Invalid unsubscribe parameters"
1142 |     );
1143 | 
1144 |     const subscription = this.activeSubscriptions.get(params.subscriptionId);
1145 |     if (!subscription) {
1146 |       throw new McpError(
1147 |         ErrorCode.InvalidParams,
1148 |         `Subscription not found: ${params.subscriptionId}`
1149 |       );
1150 |     }
1151 | 
1152 |     subscription.unsubscribe();
1153 |     this.activeSubscriptions.delete(params.subscriptionId);
1154 |     return { success: true };
1155 |   }
1156 | 
1157 |   private async handleGetNftsForOwner(args: Record<string, unknown>) {
1158 |     const params = this.validateAndCastParams<GetNftsForOwnerParams>(
1159 |       args,
1160 |       isValidGetNftsForOwnerParams,
1161 |       "Invalid NFTs for owner parameters"
1162 |     );
1163 |     return await this.alchemy.nft.getNftsForOwner(params.owner, params);
1164 |   }
1165 | 
1166 |   private async handleGetNftMetadata(args: Record<string, unknown>) {
1167 |     const params = this.validateAndCastParams<GetNftMetadataParams>(
1168 |       args,
1169 |       isValidGetNftMetadataParams,
1170 |       "Invalid NFT metadata parameters"
1171 |     );
1172 |     return await this.alchemy.nft.getNftMetadata(
1173 |       params.contractAddress,
1174 |       params.tokenId,
1175 |       params
1176 |     );
1177 |   }
1178 | 
1179 |   public async start() {
1180 |     try {
1181 |       const transport = new StdioServerTransport();
1182 |       await this.server.connect(transport);
1183 |       console.error("[Setup] Alchemy MCP server started");
1184 |     } catch (error) {
1185 |       console.error("[Server Start Error]", error);
1186 |       throw error; // or handle it differently based on your needs
1187 |     }
1188 |   }
1189 | }
1190 | 
1191 | // Start the server
1192 | const server = new AlchemyMcpServer();
1193 | server.start().catch((error) => {
1194 |   console.error("[Fatal Error]", error);
1195 |   process.exit(1);
1196 | });
1197 | 
```