// inside head tag

Building Your First Starknet MCP Server

Starknet

April 23, 2025

Connecting Blockchain to AI Agents

Today most powerful AI assistants like Claude have one massive blind spot – they can't see blockchain data because it's not part of their training data.

In this guide, I'll show you how to fix that by building a simple but powerful bridge between AI and blockchain. While most tutorials would have you building complex multi-file systems that take hours to set up, we'll create this connection with just one file in about 15 minutes.

By the end, you'll have Claude Desktop checking Starknet wallet balances in real-time, and I guarantee it will blow your mind how simple this actually is.

What are MCPs and Why Should You Care?

The Missing Link

Before diving into code, let's understand why this project is so important.

We have incredibly smart AI assistants like Claude that can reason, write code, and analyze data... but they have a major limitation. They're essentially trapped in their training data, with no ability to access real-time information from the outside world.

MCP serves as a standardized protocol connecting AI applications (left) with data sources and tools (right), enabling bidirectional data flow across previously isolated systems.

Here's the problem we're solving: When you ask Claude, "What's the balance of my Starknet wallet?" it simply can't tell you. Not because it doesn't understand the question, but because it has no way to reach out and check the blockchain.

It's like having a brilliant advisor who's stuck in a room with no windows or doors. They can give you amazing advice based on what they already know, but they can't look outside to see what's happening right now.

On the other side, we have Starknet - this powerful blockchain with all kinds of valuable data - account balances, transaction histories, smart contract states - all updating in real-time. But this data might as well be invisible to AI assistants.

Bridging Two Worlds

This is where MCP - the Model Context Protocol - comes in. I like to think of it as installing a USB port for AI.

Here's the official definition:

"The Model Context Protocol (MCP) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions."

Let me break that down into simpler terms...

MCP is basically a standardized way for AI models to access external data and functionality. Where traditional APIs connect web services together, MCP is specifically designed to connect AI models to the outside world.

Before USB, connecting devices to computers was a nightmare - different ports, different cables, different drivers. USB standardized all that. Similarly, MCP gives us a standard way for AI to connect to external tools and data.

The Building Blocks

According to the MCP documentation, there are four key components you should understand, as we'll be using these terms in our code:

  • Servers: The foundation of MCPs. A server handles connections, manages communication, and routes messages between the AI and your code. In our project, we'll create a server that connects to Starknet.
  • Resources: These provide data to the AI - similar to GET endpoints in REST APIs. Resources load information into the AI's context without performing major computations.
  • Tools: These enable the AI to take actions - similar to POST endpoints. Tools execute code or interact with external systems. Today, we'll build a tool that checks token balances on Starknet.
  • Prompts: These are reusable templates that help guide AI interactions with your server.

For our Starknet project, we'll focus primarily on creating a Server with Tools.

The MCP architecture showing how Claude Desktop (MCP Host) communicates with MCP Servers, which then interface with various tools and data sources like APIs, code environments, and databases.

How It All Works Together

Let me break down how this actually works in practice:

First, we have MCP Servers - these are like specialized adapters that connect to specific data sources or tools. That's what we're building today - a server that connects to Starknet.

Then there are MCP Clients built into AI applications - these are like the USB ports in your computer. Claude has an MCP client built in, which lets it discover and connect to servers like the one we're making.

Finally, there are MCP Hosts like Claude Desktop - these manage the connections and handle permissions, making sure everything works securely.

The Flow of Information

Here's what happens when you ask Claude about your Starknet wallet:

  1. You ask Claude about a wallet balance
  2. Claude recognizes it needs blockchain data it doesn't have
  3. It sees our MCP server is available through its client
  4. Claude sends a request to our server with the wallet address
  5. Our server tool queries the Starknet blockchain
  6. The data flows back through the same channel to Claude
  7. Claude uses the real-time data to answer your question

From Blind to Blockchain Vision

What's powerful about this approach is that we're not just building a one-off solution. The MCP standard means our server can work with any AI assistant that supports MCP, not just Claude.

And we're not just limited to checking balances. Once we understand the pattern, we could extend this to view transaction histories, monitor smart contracts, or even initiate transactions (with proper security, of course).

But the real magic is in how this transforms the user experience. Instead of jumping between tools - checking a block explorer, then coming back to Claude - you get a seamless experience where the AI can directly access the data it needs to help you.

Why Starknet + AI via MCP is a Game-Changer

Starknet brings several capabilities that make it ideal for AI integration:

  • Scalability: Using ZK-Rollups, Starknet processes thousands of transactions per second, enabling responsive, real-time interactions between AI and blockchain data.
  • Cost-Efficiency: With transaction costs often 100x lower than Ethereum mainnet, frequent AI-blockchain interactions become financially viable, enabling continuous monitoring and fine-grained smart contract interactions.
  • Programmability: Cairo's focus on creating provable programs aligns perfectly with scenarios where AI decisions need to be trustworthy and transparent.
  • Rich Ecosystem: Starknet's landscape of tokens, protocols, and applications provides fertile ground for AI integration, from DeFi protocols to NFT platforms.

When we connect these capabilities to AI through MCP, we unlock powerful possibilities:

  • AI assistants can monitor wallet balances, track transactions, and alert users to important events in their digital asset portfolio
  • Complex blockchain interactions can be initiated through simple natural language requests
  • New application categories emerge, like personalized agents that optimize DeFi positions based on market conditions

This integration transforms the user experience by replacing complex interfaces and technical operations with natural conversations with AI assistants that understand both your intentions and the technical details needed to fulfill them.

Now that we understand what we're building and why it matters, let's start coding this bridge between AI and blockchain.

Project Setup

Let's start by setting up our project environment. We'll need Node.js and npm installed.

bash
# Create project folder
mkdir starknet-mcp-demo
cd starknet-mcp-demo

# Initialize npm project
npm init -y

# Install dependencies
npm install @modelcontextprotocol/sdk starknet
npm install --save-dev typescript @types/node

# Create TypeScript configuration
echo '{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "outDir": "./dist",
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}' > tsconfig.json

Next, let's update our package.json to add two important properties:

  1. Set "type": "module" to use ES modules
  2. Add a build script

Open package.json and add these lines:

json
{
  "type": "module",
  "scripts": {
    "build": "tsc"
  }
}

The Code: Building our MCP Server

Now, let's create our MCP server in a single file. Create index.ts in your project root and add the following code:

typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { Contract, RpcProvider } from 'starknet';

// Starknet Provider
const provider = new RpcProvider({
  nodeUrl: 'https://starknet-mainnet.public.blastapi.io/rpc/v0_7'
});

// ETH and STRK token addresses
const TOKENS = {
  ETH: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7',
  STRK: '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d'
};

// Simple ERC20 ABI for checking balances
const ERC20_ABI = [
  {
    "name": "balanceOf",
    "type": "function",
    "inputs": [{ "name": "account", "type": "felt" }],
    "outputs": [{ "name": "balance", "type": "Uint256" }],
    "stateMutability": "view"
  }
];

// Check if an address is valid Starknet address
function isValidStarknetAddress(address: string): boolean {
  return /^0x[0-9a-fA-F]{64}$/.test(address);
}

// Format decimal for human readability
function formatBalance(valueStr: string): string {
  if (!valueStr || valueStr === '0') return '0';

// Assuming 18 decimals (ETH and STRK both use 18)
  const decimals = 18;

  if (valueStr.length <= decimals) {
    return `0.${valueStr.padStart(decimals, '0')}`;
  }

  const integerPart = valueStr.slice(0, valueStr.length - decimals);
  const fractionalPart = valueStr.slice(valueStr.length - decimals);

  return `${integerPart}.${fractionalPart}`;
}

// Check wallet balance for a token
async function checkBalance(walletAddress: string, tokenSymbol: string) {
  const tokenAddress = TOKENS[tokenSymbol as keyof typeof TOKENS];

// Create contract instance
  const contract = new Contract(ERC20_ABI, tokenAddress, provider);

// Call balanceOf function
  const response = await contract.balanceOf(walletAddress);
  const rawBalance = response.balance.toString();

  return {
    token: tokenSymbol,
    balance: formatBalance(rawBalance)
  };
}

// Create MCP server
const server = new Server(
  { name: "starknet-reader", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

// Define available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "check_balance",
        description: "Check token balance for a Starknet wallet address",
        inputSchema: {
          type: "object",
          properties: {
            address: {
              type: "string",
              description: "The Starknet wallet address to check"
            },
            token: {
              type: "string",
              description: "Token symbol (ETH or STRK)"
            }
          },
          required: ["address", "token"]
        }
      }
    ]
  };
});

// Implement tool functionality
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name !== "check_balance") {
    throw new Error(`Unknown tool: ${name}`);
  }

  try {
    const address = String(args?.address);
    const token = String(args?.token).toUpperCase();

// Validate inputs
    if (!address) throw new Error("Address is required");
    if (!token) throw new Error("Token is required");
    if (!isValidStarknetAddress(address)) throw new Error("Invalid Starknet address");
    if (token !== "ETH" && token !== "STRK") throw new Error("Supported tokens: ETH, STRK");

    const result = await checkBalance(address, token);

    return {
      content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : 'Unknown error';
    return {
      content: [{ type: "text", text: `Error: ${errorMessage}` }],
      isError: true
    };
  }
});

// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Starknet Reader MCP Server running");
}

main().catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});

Code Breakdown: Understanding Each Part

Let's break down this code to understand what each section does:

1. Imports and Provider Setup

typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { Contract, RpcProvider } from 'starknet';

// Starknet Provider
const provider = new RpcProvider({
  nodeUrl: 'https://starknet-mainnet.public.blastapi.io/rpc/v0_7'
});

First, we need to import the necessary libraries. The MCP SDK gives us the Server class and StdioServerTransport which handle the communication between Claude and our code. We also import the schema definitions that define the structure of our requests and responses.

From the Starknet library, we need the Contract class to interact with token contracts and the RpcProvider to connect to the network.

The provider we create connects to Starknet's mainnet using a public RPC endpoint. This gives us a way to read blockchain data without running our own node.


2. Token Definitions and Contract ABI

typescript
// ETH and STRK token addresses
const TOKENS = {
  ETH: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7',
  STRK: '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d'
};

// Simple ERC20 ABI for checking balances
const ERC20_ABI = [
  {
    "name": "balanceOf",
    "type": "function",
    "inputs": [{ "name": "account", "type": "felt" }],
    "outputs": [{ "name": "balance", "type": "Uint256" }],
    "stateMutability": "view"
  }
];

Next, we define the token contract addresses for ETH and STRK - the main tokens on Starknet. Starknet addresses are 64 hexadecimal characters after the 0x prefix, which is longer than Ethereum's 40 characters.

We also define a minimal ABI, which is essentially a description of the functions available on these token contracts. Since we only need to check balances, we're including just the balanceOf function. This function takes an account address and returns the token balance for that address.

3. Utility Functions

typescript
// Check if an address is valid Starknet address
function isValidStarknetAddress(address: string): boolean {
  return /^0x[0-9a-fA-F]{64}$/.test(address);
}

// Format decimal for human readability
function formatBalance(valueStr: string): string {
  if (!valueStr || valueStr === '0') return '0';

// Assuming 18 decimals (ETH and STRK both use 18)
  const decimals = 18;

  if (valueStr.length <= decimals) {
    return `0.${valueStr.padStart(decimals, '0')}`;
  }

  const integerPart = valueStr.slice(0, valueStr.length - decimals);
  const fractionalPart = valueStr.slice(valueStr.length - decimals);

  return `${integerPart}.${fractionalPart}`;
}

Now we define two helper functions. The formatBalance function converts the raw number format from the blockchain into something readable. Token amounts on the blockchain are stored as integers without decimal points. For ETH and STRK, 1 whole token is represented as 1 followed by 18 zeros. This function inserts the decimal point in the right place.

The isValidStarknetAddress function is a simple validation check that ensures addresses match the expected Starknet format - a 0x prefix followed by exactly 64 hexadecimal characters. This helps prevent errors when querying the blockchain.

4. Balance Checking Function

typescript
// Check wallet balance for a token
async function checkBalance(walletAddress: string, tokenSymbol: string) {
  const tokenAddress = TOKENS[tokenSymbol as keyof typeof TOKENS];

// Create contract instance
  const contract = new Contract(ERC20_ABI, tokenAddress, provider);

// Call balanceOf function
  const response = await contract.balanceOf(walletAddress);
  const rawBalance = response.balance.toString();

  return {
    token: tokenSymbol,
    balance: formatBalance(rawBalance)
  };
}

Here's the function that does the actual blockchain interaction. It takes a wallet address and token symbol as inputs, then looks up the contract address for that token from our TOKENS object.

We create a contract instance using the ABI we defined earlier, the token's address, and our provider. This gives us a JavaScript interface to the token contract.

Then we call the balanceOf function on this contract, passing the wallet address. This is an asynchronous call that fetches data directly from the blockchain.

Finally, we format the response using our formatting function and return the result with both the token symbol and the formatted balance.

5. MCP Server Setup & Tool Definition

// Create MCP server - the bridge between AI and blockchain
const server = new Server(
  { name: "starknet-reader", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

// Define available tools - telling Claude what we can do
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "check_balance",
        description: "Check token balance for a Starknet wallet address",
        inputSchema: {
          type: "object",
          properties: {
            address: {
              type: "string",
              description: "The Starknet wallet address to check"
            },
            token: {
              type: "string",
              description: "Token symbol (ETH or STRK)"
            }
          },
          required: ["address", "token"]
        }
      }
    ]
  };
});

Now we create the MCP server itself. First, we initialize it with a name and version, and specify that it will provide tools.

Then we define what tools our server will offer. We implement a handler for the ListToolsRequest, which is called when an AI like Claude wants to know what capabilities our server has.

We define a single tool called "check_balance" that accepts two parameters - an address and a token symbol. The schema definition includes descriptions of each parameter to help Claude understand how to use them. We also mark both parameters as required to ensure Claude provides all the necessary information.

This is essentially the menu of capabilities that Claude will see when it connects to our server..

6. Tool Implementation

typescript
// Implement tool functionality
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name !== "check_balance") {
    throw new Error(`Unknown tool: ${name}`);
  }

  try {
    const address = String(args?.address);
    const token = String(args?.token).toUpperCase();

// Validate inputs
    if (!address) throw new Error("Address is required");
    if (!token) throw new Error("Token is required");
    if (!isValidStarknetAddress(address)) throw new Error("Invalid Starknet address");
    if (token !== "ETH" && token !== "STRK") throw new Error("Supported tokens: ETH, STRK");

    const result = await checkBalance(address, token);

    return {
      content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : 'Unknown error';
    return {
      content: [{ type: "text", text: `Error: ${errorMessage}` }],
      isError: true
    };
  }
});

Next, we implement the handler that executes when Claude calls our tool. This handler receives the request from Claude, processes it, and returns the result.

First, we extract the tool name and arguments from the request. We verify that the tool name is "check_balance" since that's the only tool we support.

We then validate the inputs - making sure the address and token parameters are provided, the address follows the correct Starknet format, and the token is either ETH or STRK.

If everything checks out, we call our checkBalance function with the provided parameters and return the result as a formatted JSON string.

We also implement error handling to provide clear error messages if anything goes wrong during the process. This helps Claude understand what happened and potentially retry with corrected parameters.

7. Server Startup

typescript
// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Starknet Reader MCP Server running");
}

main().catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});

Finally, we start the server. We create a StdioServerTransport which handles communication between Claude Desktop and our server using standard input/output streams.

We connect our server to this transport and log a message indicating that the server is running. Note that we use console.error because the standard output is reserved for the MCP protocol communication.

We also add error handling to catch any unexpected errors that might occur during server operation. If a fatal error happens, we log it and exit the process.

And that's it! With just about 100 lines of code, we've created a complete bridge between Claude and the Starknet blockchain. The entire implementation is clean, focused, and handles all the necessary communication between AI and blockchain.

Building and Running the Server

Now that we've written our code, let's build and run it:

bash
# Add the build script to package.json first if you haven't already# "scripts": { "build": "tsc" }# Build the TypeScript code
npm run build

Configuring Claude to Use Our Server

To use our MCP server with Claude:

  1. Open Claude Desktop
  2. Click on the Settings icon
  3. Navigate to the Developer section
  4. Click "Edit Config" in the MCP Servers section
  5. Add your server configuration:

json
{
  "mcpServers": {
    "starknet-reader": {
      "command": "node",
      "args": ["/full/path/to/starknet-mcp-demo/dist/index.js"]
    }
  }
}

Replace /full/path/to/starknet-mcp-demo/dist/index.js with the actual path to your compiled JavaScript file.

  1. Save the configuration
  2. Toggle the switch next to "starknet-reader" to enable it


Testing Our Server

Now you can ask Claude to check token balances on Starknet. Try questions like:

  • "What's the ETH balance for this Starknet address: 0x0736a05dc46efaf497f14a59464e50f8c02d41e4cdc4b12d0fdffd9342872f7e?"
  • "Can you check the STRK balance for wallet 0x0736a05dc46efaf497f14a59464e50f8c02d41e4cdc4b12d0fdffd9342872f7e on Starknet?"

Claude will use your MCP server to fetch real-time data from the Starknet blockchain!

Expanding the Server

This minimal server can be expanded in many ways:

  • Add support for more tokens
  • Implement transaction history checking
  • Add price data to convert token amounts to USD
  • Enable interaction with smart contracts
  • Support other networks like Starknet testnet


Conclusion

In this tutorial, we've built a minimal but functional MCP server that connects AI assistants like Claude to the Starknet blockchain. With just a single file, we've created a bridge between two revolutionary technologies.

This integration pattern opens up exciting possibilities, from AI-powered DeFi assistants to personalized blockchain analytics. As both AI and blockchain technologies continue to evolve, these kinds of integrations will become increasingly powerful and important.

By understanding how to build these bridges today, you're positioning yourself at the forefront of what's likely to be a significant technological trend in the coming years.

Happy coding!

Latest articles