Token-gate your MCP tools, resources, and prompts with just 3 lines of code. Works seamlessly with the Radius MCP Server to enable decentralized, token-based access control across the MCP ecosystem.
TESTNET RELEASE
This SDK is currently configured for use with Radius Testnet.
The Radius MCP SDK is the authorization component of the Radius ecosystem. It allows MCP server developers to protect their tools, resources, and prompts with ERC-1155 token requirements. The SDK verifies cryptographic proofs generated by the Radius MCP Server and checks on-chain token ownership, enabling a decentralized marketplace of token-gated AI tools.
- Simple Integration: Add SDK to your MCP server with 3 lines of code
- Proof Verification: SDK validates EIP-712 signatures from the Radius MCP Server
- Token Checking: Direct on-chain verification of token ownership
- Intelligent Errors: Guides Claude through authentication and purchase flow
- Performance: Built-in caching and request deduplication
pnpm add @radiustechsystems/mcp-sdk
# or
npm install @radiustechsystems/mcp-sdk
# or
yarn add @radiustechsystems/mcp-sdk
- 🔐 Cryptographic Security: EIP-712 signature verification
- 🧠 AI-Friendly: Structured error responses that guide Claude through the flow
- 📝 TypeScript First: Full type safety with comprehensive interfaces
- 🛡️ Security Enhanced: Chain ID validation and replay protection
- ⚡ High Performance: Built-in caching and request deduplication
- 🎯 Simple Integration: Just 3 lines of code to protect any MCP tool
import { RadiusMcpSdk } from '@radiustechsystems/mcp-sdk';
// Simple usage - just provide the contract address!
// Defaults to Radius Testnet (chainId: 1223953)
const radius = new RadiusMcpSdk({
contractAddress: '0x5448Dc20ad9e0cDb5Dd0db25e814545d1aa08D96'
});
// Protect any MCP tool, resource, or prompt
handler: radius.protect(TOKEN_ID, yourHandler)
Check out the /examples/fastmcp
directory for a complete working example that you can run in under 60 seconds!
The SDK has minimal dependencies:
-
viem
- For Ethereum interactions and EIP-712 signature verification -
@evmauth/eip712-authn
- For EIP-712 authentication handling
// 1. Claude tries to use your protected tool
await yourTool({ query: "analyze market data" });
// Response: EVMAUTH_PROOF_MISSING error with required tokens
// 2. Claude calls Radius MCP Server (handles everything in one call!)
const response = await authenticate_and_purchase({
tokenIds: [101] // From error's requiredTokens field
});
// Returns: { proof, purchases } - proof for auth + purchase info (if tokens were bought)
// 3. Claude retries with authentication proof
const result = await yourTool({
query: "analyze market data",
__evmauth: response.proof // Proof from authenticate_and_purchase
});
// Success! Tool executes and returns results
The SDK uses a reserved __evmauth
namespace to:
- Keep auth separate: Authentication data never mixes with business logic
- Maintain full security: EIP-712 signature verification with cryptographic guarantees
- Simplify handlers: Your tool handlers receive clean requests without auth data
- Guide Claude: Clear error messages show exactly how to use the namespace
// Your entire integration:
const radius = new RadiusMcpSdk({ ...config });
server.addTool({
name: 'market_analyzer',
handler: radius.protect(101, async (args) => {
// Your tool logic here - only runs if user owns token 101
return analyzeMarket(args.query);
})
});
Chain ID: 1223953
RPC URL: https://rpc.testnet.radiustech.xyz
const radius = new RadiusMcpSdk({
contractAddress: '0x5448Dc20ad9e0cDb5Dd0db25e814545d1aa08D96'
});
const radius = new RadiusMcpSdk({
contractAddress: '0x5448Dc20ad9e0cDb5Dd0db25e814545d1aa08D96',
chainId: 1223953,
rpcUrl: 'https://rpc.testnet.radiustech.xyz',
cache: {
ttl: 300, // Cache TTL in seconds
maxSize: 1000, // Max cache entries
disabled: false // Set true to disable caching
},
debug: false // Enable debug logging
});
Option | Type | Default | Description |
---|---|---|---|
contractAddress |
string |
Required | ERC-1155 contract address |
chainId |
number |
1223953 |
Blockchain network ID (Radius Testnet) |
rpcUrl |
string |
Radius Testnet RPC | RPC endpoint for ownership checks |
cache.ttl |
number |
60 |
Cache TTL in seconds |
cache.maxSize |
number |
1000 |
Maximum cache entries |
cache.disabled |
boolean |
false |
Disable caching |
debug |
boolean |
false |
Enable debug logging |
import { FastMCP } from 'fastmcp';
import { RadiusMcpSdk } from '@radiustechsystems/mcp-sdk';
import { z } from 'zod';
// Simple initialization - defaults to Radius Testnet
const radius = new RadiusMcpSdk({
contractAddress: '0x5448Dc20ad9e0cDb5Dd0db25e814545d1aa08D96'
});
// Or explicitly set chain/RPC for other networks
const radiusCustom = new RadiusMcpSdk({
contractAddress: '0x5448Dc20ad9e0cDb5Dd0db25e814545d1aa08D96',
chainId: 1223953,
rpcUrl: 'https://rpc.testnet.radiustech.xyz'
});
const server = new FastMCP({
name: 'Premium Analytics',
version: '1.0.0'
});
// Protected tool
server.addTool({
name: 'premium_analytics',
description: 'Advanced market analytics (requires token)',
inputSchema: z.object({
market: z.string(),
timeframe: z.string()
}),
handler: radius.protect(101, async (args) => {
const data = await fetchPremiumData(args.market, args.timeframe);
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
})
});
// Protected resource
server.addResource({
name: 'premium_dataset',
description: 'Premium market dataset (requires token)',
handler: radius.protect(102, async () => {
return {
contents: [
{ uri: 'dataset://premium/2024', text: loadPremiumData() }
]
};
})
});
// Protected prompt
server.addPrompt({
name: 'expert_trading_prompt',
description: 'Expert trading strategies (requires token)',
handler: radius.protect(103, async () => {
return {
messages: [
{ role: 'system', content: 'You are an expert trader...' },
{ role: 'user', content: 'Analyze this market...' }
]
};
})
});
// User needs ANY of these tokens
handler: radius.protect([201, 202, 203], async (args) => {
// User has at least one of the required tokens
return performEnterpriseAnalytics(args.query);
})
// Different tools require different tokens
const TOKENS = {
BASIC_ANALYTICS: 101,
PREMIUM_ANALYTICS: 102,
ENTERPRISE_ANALYTICS: [201, 202, 203] // ANY of these
};
// Basic tier
server.addTool({
name: 'basic_analytics',
handler: radius.protect(TOKENS.BASIC_ANALYTICS, basicHandler)
});
// Premium tier
server.addTool({
name: 'premium_analytics',
handler: radius.protect(TOKENS.PREMIUM_ANALYTICS, premiumHandler)
});
// Enterprise tier
server.addTool({
name: 'enterprise_analytics',
handler: radius.protect(TOKENS.ENTERPRISE_ANALYTICS, enterpriseHandler)
});
This SDK works in tandem with the Radius MCP Server to create a complete token-gating ecosystem:
-
Radius MCP Server
- Handles OAuth authentication with AI clients
- Manages user wallets via Privy
- Generates cryptographic proofs
- Processes token purchases
- One instance per AI client
-
Radius MCP SDK (this repo)
- Verifies proofs from the Radius MCP Server
- Checks on-chain token ownership
- Protects your MCP tools/resources/prompts
- Guides AI through the flow
The SDK uses specific error codes for different failure scenarios:
-
EVMAUTH_PROOF_MISSING
- No proof provided in the request -
PROOF_EXPIRED
- Proof has expired (proofs expire after 30 seconds by default) -
PROOF_INVALID
- Proof format is invalid -
CHAIN_MISMATCH
- Chain ID in proof doesn't match SDK configuration -
CONTRACT_MISMATCH
- Contract address in proof doesn't match SDK configuration -
SIGNATURE_INVALID
- EIP-712 signature verification failed -
SIGNER_MISMATCH
- Signature doesn't match the claimed wallet address -
NONCE_INVALID
- Nonce is malformed or outside acceptable time window -
PAYMENT_REQUIRED
- User doesn't own required tokens
The SDK provides comprehensive error responses that guide MCP clients such as Claude through the authentication flow:
// When proof is missing
{
"error": {
"code": "EVMAUTH_PROOF_MISSING",
"message": "You need to authenticate with Radius MCP Server first",
"details": {
"contractAddress": "0x5448Dc20ad9e0cDb5Dd0db25e814545d1aa08D96",
"chainId": 1223953,
"requiredTokens": [101]
},
"claude_action": {
"description": "You need to authenticate and potentially purchase tokens",
"steps": [
"Call the authenticate_and_purchase tool on Radius MCP Server with the required tokenIds",
"The tool will check if you own the tokens and purchase them if needed",
"Copy the entire proof object from the response",
"Include it as \"__evmauth\": <proof> in this tool's arguments",
"Retry this tool call with the proof included"
],
"tool": {
"server": "radius-mcp-server",
"name": "authenticate_and_purchase",
"arguments": {
"tokenIds": [101]
}
}
}
}
}
// When token ownership is required (after authentication)
{
"error": {
"code": "PAYMENT_REQUIRED",
"message": "Token ownership required",
"details": {
"requiredTokens": [101],
"contractAddress": "0x5448Dc20ad9e0cDb5Dd0db25e814545d1aa08D96",
"chainId": 1223953,
"checkedWallet": "0x742d35Cc6634C0532925a3b844Bc9e7Ed1A0aC0E"
},
"claude_action": {
"description": "You don't own the required tokens. You need to purchase them.",
"steps": [
"Call the authenticate_and_purchase tool again on Radius MCP Server",
"It will automatically purchase the missing tokens",
"Use the new proof from the response",
"Retry this tool call with the new proof"
],
"tool": {
"server": "radius-mcp-server",
"name": "authenticate_and_purchase",
"arguments": {
"tokenIds": [101]
}
}
}
}
}
The SDK uses a reserved __evmauth
namespace for authentication data. This design ensures:
- Tool handlers receive clean requests without authentication parameters
- Authentication logic is completely separated from business logic
- Full cryptographic security with EIP-712 signatures
- Claude receives clear guidance on how to provide authentication
The SDK implements a multi-layered security approach:
- EIP-712 Signature Verification: Validates cryptographic proofs using standard Ethereum signing
- Chain ID Validation: Prevents cross-chain replay attacks
- Domain Validation: Ensures proof was created for the correct contract
- Timestamp Validation: Prevents replay of expired proofs
- Nonce Validation: Ensures proof freshness and prevents replay attacks
- Fail-Closed Design: Denies access on any validation failure
- Intelligent Caching: LRU cache with TTL for token ownership results
- Request Deduplication: Prevents duplicate RPC calls for same token/wallet
- Connection Pooling: Efficient RPC client with retry logic
sequenceDiagram
participant C as Claude
participant T as Tool (Your MCP)
participant S as Radius MCP SDK
participant R as RPC Node
participant E as Radius MCP Server
C->>T: Call protected tool
T->>S: protect() wrapper
S->>S: Extract proof from request
alt No proof
S->>C: EVMAUTH_PROOF_MISSING
C->>E: authenticate_and_purchase
E->>C: Return proof and purchase info
C->>T: Retry with proof
end
S->>S: Verify EIP-712 signature
S->>S: Validate timestamp and nonce
S->>S: Check cache for token
alt Cache miss
S->>R: balanceOf(wallet, tokenId)
R->>S: Return balance
S->>S: Cache result
end
alt Has tokens
S->>T: Execute handler
T->>C: Return result
else No tokens
S->>C: PAYMENT_REQUIRED
C->>E: authenticate_and_purchase
E->>C: Purchase complete with new proof
C->>T: Retry with new proof
end
The SDK exports the following types and utilities:
// Main SDK class
export { RadiusMcpSdk }
// Types
export type {
RadiusConfig,
CacheConfig,
MCPHandler,
MCPRequest,
MCPResponse,
EVMAuthProof,
EVMAuthErrorResponse,
ProofErrorCode,
RadiusError,
RadiusErrorCode
}
// Utilities from viem
export { isAddress }
export type { Address }
// Version constant
export const VERSION
import { RadiusMcpSdk, type RadiusConfig, type EVMAuthProof, VERSION } from '@radiustechsystems/mcp-sdk';
// Version constant is exported
console.log('SDK Version:', VERSION); // '1.0.0'
// Configuration is fully typed
const config: RadiusConfig = {
contractAddress: '0x5448Dc20ad9e0cDb5Dd0db25e814545d1aa08D96',
chainId: 1223953,
rpcUrl: 'https://rpc.testnet.radiustech.xyz',
cache: {
ttl: 300,
maxSize: 1000,
disabled: false
}
};
// Proof structure is typed
const proof: EVMAuthProof = {
challenge: {
domain: {
name: 'EVMAuth',
version: '1',
chainId: 1223953,
verifyingContract: '0x5448Dc20ad9e0cDb5Dd0db25e814545d1aa08D96'
},
types: {
EIP712Domain: [/* ... */],
EVMAuthRequest: [/* ... */]
},
primaryType: 'EVMAuthRequest',
message: {
serverName: 'radius-mcp-server',
resourceName: 'mcp_tool',
requiredTokens: '[]',
walletAddress: '0x...',
nonce: '123456-randomhex',
issuedAt: '1234567890',
expiresAt: '1234567890',
purpose: 'mcp_tool_access'
}
},
signature: '0x...' as `0x${string}`
};
import type { MCPHandler, MCPRequest, MCPResponse } from '@radiustechsystems/mcp-sdk';
// Your handlers are properly typed
const myHandler: MCPHandler = async (
request: MCPRequest,
extra?: any
): Promise<MCPResponse> => {
// TypeScript knows the structure
return {
content: [{ type: 'text', text: 'Hello!' }]
};
};
The RadiusMcpSdk
class exposes only one public method:
-
protect(tokenId: number | number[], handler: MCPHandler): MCPHandler
- Wraps a handler with token protection
All other functionality (proof verification, token checking, caching) is handled internally and not exposed in the public API.
// Token requirements based on user tier
const getTokenRequirement = (userTier: string): number | number[] => {
switch (userTier) {
case 'basic': return 101;
case 'premium': return 102;
case 'enterprise': return [201, 202, 203];
default: throw new Error('Invalid tier');
}
};
server.addTool({
name: 'tiered_analytics',
handler: async (request) => {
const userTier = request.params.tier;
const tokenRequirement = getTokenRequirement(userTier);
return radius.protect(tokenRequirement, async (args) => {
return performTieredAnalysis(args, userTier);
})(request);
}
});
import { RadiusMcpSdk } from '@radiustechsystems/mcp-sdk';
import { vi, describe, it, expect } from 'vitest';
describe('Radius MCP SDK', () => {
it('should require proof for protected handlers', async () => {
const radius = new RadiusMcpSdk({
contractAddress: '0x5448Dc20ad9e0cDb5Dd0db25e814545d1aa08D96',
chainId: 1223953,
rpcUrl: 'https://rpc.testnet.radiustech.xyz'
});
const handler = vi.fn();
const protectedHandler = radius.protect(101, handler);
const request = { params: { arguments: {} } };
const response = await protectedHandler(request);
expect(response.content[0].text).toContain('EVMAUTH_PROOF_MISSING');
expect(handler).not.toHaveBeenCalled();
});
});
const radius = new RadiusMcpSdk({
contractAddress: '0x...',
chainId: 1223953,
rpcUrl: 'https://...',
debug: true // Enable debug logging
});
// Debug mode logs:
// - Token balance check results
// - Cache hit/miss information
// - RPC call details
// - Proof validation steps
-
"EVMAUTH_PROOF_MISSING" Errors
- Ensure Radius MCP Server is connected
- Check that proof is included in
__evmauth
parameter - Verify proof hasn't expired (default 30 seconds)
-
"Chain ID mismatch" Errors
- Verify SDK chainId matches the network
- Ensure proof was created for the same chain
-
RPC Connection Issues
- Verify RPC URL is correct and accessible
- Check network connectivity
- Ensure RPC endpoint supports eth_call
When using tools protected by Radius MCP SDK:
-
The
__evmauth
parameter is ALWAYS accepted - Even if a tool's schema doesn't list__evmauth
as a parameter, ALL Radius MCP SDK-protected tools accept it. -
It's automatically stripped - The SDK removes the
__evmauth
parameter before calling the actual tool logic, so tool developers never see it. -
Example usage:
// First, authenticate and get a proof const { proof } = await authenticate_and_purchase({ tokenIds: [101] // Required tokens from the error }); // Then use it with ANY protected tool const result = await any_protected_tool({ // ... normal parameters ..., __evmauth: proof // This ALWAYS works! });
-
Common mistake: Don't worry if you don't see
__evmauth
in a tool's parameter list. It's a special parameter that's always accepted by the Radius MCP SDK.