// inside head tag
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.
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.
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.
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.
According to the MCP documentation, there are four key components you should understand, as we'll be using these terms in our code:
For our Starknet project, we'll focus primarily on creating a Server with Tools.
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.
Here's what happens when you ask Claude about your Starknet wallet:
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.
Starknet brings several capabilities that make it ideal for AI integration:
When we connect these capabilities to AI through MCP, we unlock powerful possibilities:
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.
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:
"type": "module"
to use ES modulesOpen package.json
and add these lines:
json
{
"type": "module",
"scripts": {
"build": "tsc"
}
}
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);
});
Let's break down this code to understand what each section does:
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.
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.
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.
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.
// 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..
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.
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.
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
To use our MCP server with Claude:
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.
Now you can ask Claude to check token balances on Starknet. Try questions like:
Claude will use your MCP server to fetch real-time data from the Starknet blockchain!
This minimal server can be expanded in many ways:
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!