# 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 | [](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 |
```