JSON-RPC Tool Generation
This guide explains how to automatically generate OpenPets-compatible tools from JSON-RPC/OpenRPC specifications using the dedicated JSON-RPC generator in src/core/jsonrpc-generator.ts.
CRITICAL: ALL tools for JSON-RPC-based pets must be generated using the
pets generate-jsonrpcCLI command with the@src/core/jsonrpc-generator.tstool. This ensures consistency, proper RPC request handling, and compatibility with OpenPets standards.
Why Use the Official Generator
The @src/core/jsonrpc-generator.ts is the canonical way to generate JSON-RPC tools because it:
- Ensures Consistency - All JSON-RPC pets use the same generation patterns
- Proper RPC Handling - Handles JSON-RPC 2.0 request/response protocol correctly
- OpenPets Integration - Includes read-only mode, error handling, logging
- Schema Mapping - Converts OpenRPC schemas to Zod types
- Future-Proof - Updates to generator benefit all JSON-RPC pets
REQUIRED: Use pets generate-jsonrpc CLI
ALWAYS generate JSON-RPC tools using the official CLI:
# NEVER write tools manually for JSON-RPC specs
cd pets/ethereum-publicnode
pets generate-jsonrpc
# NOT this:
# ❌ Manual tool writing (error-prone)
# ❌ Custom scripts (may break compatibility)
# ❌ Other generators (lack OpenPets features)The CLI automatically:
- Uses the
@src/core/jsonrpc-generator.tsimplementation - Reads your
package.jsonjsonrpcSpec configuration - Generates proper OpenPets-compatible tools
- Creates
jsonrpc-client.tswith all tools - Handles JSON-RPC 2.0 protocol and read-only mode
Working Example: Ethereum PublicNode Pet
The pets/ethereum-publicnode pet demonstrates the complete JSON-RPC generation workflow:
Files Created:
jsonrpc-client.ts- Auto-generated tools (45 Ethereum RPC methods)- Updated
package.jsonwith jsonrpcSpec configuration - Updated
index.tsto import generated tools
Generated Tools:
ethereum-publicnode-eth-blocknumber- Get current block numberethereum-publicnode-eth-getbalance- Get account balanceethereum-publicnode-eth-call- Call smart contract (read-only)ethereum-publicnode-net-version- Get network ID- ...and 41 more Ethereum JSON-RPC methods
Generation Process:
cd pets/ethereum-publicnode
pets generate-jsonrpc --verbose
✅ Generated 45 tools (31 read-only, 14 write) to jsonrpc-client.tsKey Features Demonstrated:
- No authentication required (free public endpoint)
- Proper JSON-RPC 2.0 request formatting
- Read-only mode support
- Hex/number parameter conversion
- Structured error responses
This example shows the complete, production-ready approach for JSON-RPC-based pets.
Overview
JSON-RPC is a stateless, light-weight remote procedure call (RPC) protocol commonly used by blockchain nodes (Ethereum, Bitcoin) and other distributed systems. Instead of REST endpoints, JSON-RPC uses a single endpoint with POST requests containing method names and parameters.
This approach is ideal for:
- Blockchain RPC nodes (Ethereum, Bitcoin, Polygon, etc.)
- JSON-RPC 2.0 APIs with OpenRPC specifications
- Services that provide method-based APIs rather than resource-based REST
- Quickly bootstrapping a pet for RPC-based services
When to Use JSON-RPC Generation
| Approach | Best For |
|---|---|
| JSON-RPC Generation | Blockchain nodes, RPC APIs with OpenRPC specs |
| OpenAPI Generation | REST APIs with OpenAPI specs, API-first products |
| MCP Generation | Services with official MCP servers |
| Manual Tools | Custom logic, non-standard protocols |
JSON-RPC vs REST
| Feature | JSON-RPC | REST |
|---|---|---|
| Endpoints | Single endpoint (POST) | Multiple endpoints (GET/POST/PUT/DELETE) |
| Method | In request body (method field) | In HTTP verb + URL path |
| Spec Format | OpenRPC | OpenAPI/Swagger |
| Common Use | Blockchain, distributed systems | Web APIs, SaaS platforms |
| Parameters | Array in params field | URL params + request body |
Quick Start
Option 1: From Command Line
cd pets/my-rpc-service
# Generate from URL (PREFERRED - always gets latest spec)
pets generate-jsonrpc --url https://raw.githubusercontent.com/etclabscore/ethereum-json-rpc-specification/master/openrpc.json
# Generate from local file (only if URL not available)
pets generate-jsonrpc --file ./openrpc.json
# Preview without writing
pets generate-jsonrpc --url https://example.com/openrpc.json --dry-run --verboseOption 2: From package.json Config (Recommended)
Add a jsonrpcSpec section to your pet's package.json:
{
"name": "@openpets/my-rpc-service",
"jsonrpcSpec": {
"url": "https://example.com/openrpc.json",
"baseUrl": "https://rpc.example.com"
}
}Then run:
cd pets/my-rpc-service
pets generate-jsonrpcBest Practice: Always use
urlwhen the service hosts their OpenRPC spec. This ensures:
- Tools are generated from the latest spec version
- New methods are available after regenerating
- Deprecated methods are automatically removed
- No need to manually sync local files
Configuration Reference
package.json jsonrpcSpec
| Property | Type | Required | Description |
|---|---|---|---|
url | string | Yes* | URL to fetch OpenRPC spec from |
file | string | Yes* | Local file path to OpenRPC spec |
baseUrl | string | Yes | RPC endpoint URL (e.g., https://ethereum-rpc.publicnode.com) |
authEnvVar | string | No | Environment variable for auth token/key |
authType | string | No | Authentication type: bearer, apiKey, basic, or none (default) |
include | string[] | No | Only generate tools matching these patterns |
exclude | string[] | No | Skip tools matching these patterns |
readOnlyPatterns | string[] | No | Mark tools matching these patterns as read-only |
nameCleaningPatterns | string[] | No | Regex patterns to remove from tool names |
*Either url or file is required.
CLI Options
pets generate-jsonrpc [options]
Options:
--url <url> URL to fetch OpenRPC spec from
--file <path> Local file path to OpenRPC spec
--base-url <url> RPC endpoint URL
--output <file> Output file name (default: jsonrpc-client.ts)
--dry-run, -n Preview without writing files
--verbose, -v Show detailed outputGenerated File Structure
The generator creates a jsonrpc-client.ts file with:
- Configuration - Base URL and auth from environment
- Read-only mode support - Filters write operations when enabled
- RPC wrapper - JSON-RPC 2.0 request handler with error parsing
- Tool definitions - One tool per RPC method with:
- Kebab-case name with pet prefix
- Description from method summary/description
- Zod schema with all parameters
- Execute function that calls makeRpcRequest()
Example Generated Tool
{
name: "ethereum-publicnode-eth-getbalance",
description: "Returns Ether balance of a given or account or contract",
schema: z.object({
address: z.any().describe("The address of the account or contract"),
blockNumber: z.any().describe("A BlockNumber at which to request the balance").optional()
}),
async execute(args: any) {
return makeRpcRequest("eth_getBalance", [args.address, args.blockNumber])
}
}JSON-RPC Request Handler
The generated makeRpcRequest function handles the JSON-RPC 2.0 protocol:
async function makeRpcRequest(method: string, params: any[] = []): Promise<string> {
const id = Date.now()
const payload = {
jsonrpc: "2.0",
id,
method,
params
}
try {
const response = await fetch(baseUrl, {
method: "POST",
headers,
body: JSON.stringify(payload)
})
if (!response.ok) {
const errorText = await response.text()
return JSON.stringify({
success: false,
error: `HTTP ${response.status}: ${errorText}`,
status: response.status
}, null, 2)
}
const data = await response.json()
if (data.error) {
return JSON.stringify({
success: false,
error: data.error.message || "RPC error",
code: data.error.code,
data: data.error.data
}, null, 2)
}
return JSON.stringify({
success: true,
result: data.result
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message
}, null, 2)
}
}This pattern:
- Wraps every RPC method in consistent error handling
- Returns structured JSON responses
- Handles both HTTP errors and RPC errors
- Includes proper JSON-RPC 2.0 request format
Using Generated Tools in Your Plugin
Update your index.ts to import and use the generated tools:
import { config as loadEnv } from "dotenv"
import { createPlugin, type ToolDefinition, createLogger, z } from "openpets-sdk"
import jsonRpcTools from "./jsonrpc-client"
loadEnv()
const logger = createLogger("my-rpc-service")
export const MyRpcServicePlugin = async () => {
logger.info("Initializing RPC service plugin")
const testConnectionTool: ToolDefinition = {
name: "my-rpc-service-test-connection",
description: "Test RPC connection",
schema: z.object({}),
async execute() {
try {
const baseUrl = "https://rpc.example.com"
const payload = {
jsonrpc: "2.0",
id: 1,
method: "ping",
params: []
}
const response = await fetch(baseUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
})
if (!response.ok) {
return JSON.stringify({
success: false,
status: "error",
error: `HTTP ${response.status}`
}, null, 2)
}
const data = await response.json()
return JSON.stringify({
success: true,
status: "connected",
message: "RPC service connected successfully",
details: {
endpoint: baseUrl,
totalTools: jsonRpcTools.length + 1
}
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
status: "connection_failed",
error: error.message
}, null, 2)
}
}
}
return createPlugin([testConnectionTool, ...jsonRpcTools])
}
export default MyRpcServicePluginSchema Type Mapping
The generator maps OpenRPC/JSON Schema types to Zod types:
| OpenRPC Type | Zod Type |
|---|---|
string | z.string() |
string with enum | z.enum([...]) |
number, integer | z.number() |
boolean | z.boolean() |
array of strings | z.array(z.string()) |
array of numbers | z.array(z.number()) |
array of objects | z.string() (JSON string) |
object (nested) | z.any() with description |
anyOf with null | Base type with .optional() |
Handling Complex Types
Due to OpenPets runtime schema limits, complex nested structures are serialized as JSON strings:
// Nested object becomes:
config: z.string().describe("JSON object: Configuration settings")
// Array of objects becomes:
items: z.string().describe("JSON array")Parameter Handling
JSON-RPC methods receive parameters as an array. The generator converts named parameters to positional array elements:
OpenRPC Spec:
{
"name": "eth_getBalance",
"params": [
{
"name": "address",
"schema": { "type": "string" }
},
{
"name": "blockNumber",
"schema": { "type": "string" }
}
]
}Generated Tool:
schema: z.object({
address: z.any().describe("The address of the account"),
blockNumber: z.any().describe("Block number").optional()
}),
async execute(args: any) {
return makeRpcRequest("eth_getBalance", [args.address, args.blockNumber])
}The parameters are passed in order to the RPC method.
Authentication
The generator supports multiple authentication methods, though most JSON-RPC services use no authentication:
No Authentication (Default)
{
"jsonrpcSpec": {
"url": "https://example.com/openrpc.json",
"baseUrl": "https://rpc.example.com",
"authType": "none"
}
}This is the most common setup for public RPC nodes like Ethereum PublicNode.
Bearer Token
{
"jsonrpcSpec": {
"url": "https://example.com/openrpc.json",
"baseUrl": "https://rpc.example.com",
"authType": "bearer",
"authEnvVar": "MY_RPC_API_KEY"
}
}Set MY_RPC_API_KEY in your .env file. The generator adds:
headers["Authorization"] = `Bearer ${apiKey}`API Key Header
{
"jsonrpcSpec": {
"url": "https://example.com/openrpc.json",
"baseUrl": "https://rpc.example.com",
"authType": "apiKey",
"apiKeyName": "X-API-Key"
}
}Basic Auth
{
"jsonrpcSpec": {
"url": "https://example.com/openrpc.json",
"baseUrl": "https://rpc.example.com",
"authType": "basic"
}
}Read-Only Mode
Generated tools automatically support read-only mode. When enabled:
pets read-only my-rpc-service onWrite operations are filtered out based on read-only patterns.
Default read-only patterns: get, list, eth_, net_, web3_
Custom patterns:
{
"jsonrpcSpec": {
"url": "https://example.com/openrpc.json",
"baseUrl": "https://rpc.example.com",
"readOnlyPatterns": ["get", "list", "fetch", "query", "eth_block", "eth_get"]
}
}Filtering Methods
Include Only Specific Methods
{
"jsonrpcSpec": {
"url": "https://example.com/openrpc.json",
"baseUrl": "https://rpc.example.com",
"include": ["eth_getBalance", "eth_blockNumber", "eth_call"]
}
}Exclude Methods
{
"jsonrpcSpec": {
"url": "https://example.com/openrpc.json",
"baseUrl": "https://rpc.example.com",
"exclude": ["eth_sendRawTransaction", "eth_sendTransaction"]
}
}This is useful for excluding write operations from public RPC endpoints.
Regenerating Tools
When the RPC spec updates, regenerate the tools:
cd pets/my-rpc-service
pets generate-jsonrpcThe jsonrpc-client.ts file will be regenerated with the latest methods.
Real-World Example: Ethereum PublicNode
The pets/ethereum-publicnode pet uses JSON-RPC generation for Ethereum mainnet access:
package.json:
{
"name": "@openpets/ethereum-publicnode",
"jsonrpcSpec": {
"url": "https://raw.githubusercontent.com/etclabscore/ethereum-json-rpc-specification/master/openrpc.json",
"baseUrl": "https://ethereum-rpc.publicnode.com",
"petName": "ethereum-publicnode",
"authType": "none",
"readOnlyPatterns": ["get", "list", "eth_block", "eth_get", "eth_call", "net_", "web3_"],
"exclude": ["eth_sendRawTransaction", "eth_sendTransaction"],
"generateDocs": true,
"autoTestConnection": true
}
}Generate tools:
cd pets/ethereum-publicnode
pets generate-jsonrpc --verboseOutput:
Generated 45 tools to jsonrpc-client.ts
31 read-only (eth_getBalance, eth_blockNumber, eth_call, etc.)
14 write (excluded by config)Generated Tools:
| Category | Methods |
|---|---|
| Block Queries | eth-blocknumber, eth-getblockbyhash, eth-getblockbynumber |
| Account/Balance | eth-getbalance, eth-gettransactioncount, eth-getcode |
| Transactions | eth-gettransactionbyhash, eth-gettransactionreceipt |
| Smart Contracts | eth-call, eth-estimategas |
| Network Info | eth-gasprice, net-version, web3-clientversion |
Usage Examples:
# Check current block number
bun test:pet <pet-name> --query "what's the current ethereum block number"
# Get account balance
bun test:pet <pet-name> --query "get ethereum balance for vitalik.eth"
# Call smart contract
bun test:pet <pet-name> --query "call ethereum contract at 0x... with data 0x..."
# Get gas price
bun test:pet <pet-name> --query "what's the current ethereum gas price"Key Learnings from Ethereum PublicNode
No Authentication Required: PublicNode is completely free, making it ideal for read-only blockchain queries.
Hex Number Handling: Ethereum returns numbers as hex strings (e.g.,
0x173d76b). The AI assistant can convert these or display them in both formats.Read-Only by Default: Excluding
eth_sendRawTransactionandeth_sendTransactionprevents accidental write operations on mainnet.OpenRPC Spec Quality: The ethereum-json-rpc-specification project provides a well-maintained OpenRPC spec that covers all standard Ethereum JSON-RPC methods.
Block Parameter Types: Methods like
eth_getBalanceaccept block parameters that can be:- Block number (hex string like
0x1234) - Block tags (
latest,earliest,pending) - Block hash
- Block number (hex string like
Response Consistency: All methods return structured JSON with
successandresultfields, making it easy to parse responses.
Troubleshooting
"No JSON-RPC configuration found"
Add jsonrpcSpec to your package.json or use --url/--file options.
"Failed to fetch OpenRPC spec"
- Check the URL is accessible
- Verify the spec is valid JSON
- Some specs may be YAML format (not yet supported)
Generated tools have wrong RPC endpoint
Specify baseUrl in your config:
{
"jsonrpcSpec": {
"url": "https://docs.example.com/openrpc.json",
"baseUrl": "https://rpc.example.com"
}
}Parameters not being sent correctly
JSON-RPC requires parameters in a specific order. Check the OpenRPC spec's parameter order and ensure your Zod schema matches:
// Correct - parameters in order
return makeRpcRequest("method", [args.param1, args.param2])
// Wrong - parameters out of order
return makeRpcRequest("method", [args.param2, args.param1])RPC error responses
The generator handles two types of errors:
- HTTP Errors (connection failed, 404, 500, etc.)
- RPC Errors (method not found, invalid params, etc.)
Both are returned as structured JSON:
{
"success": false,
"error": "Method not found",
"code": -32601
}Hex number conversion
Many blockchain RPC methods return hex-encoded numbers. You can convert them:
// Hex to decimal
const blockNumber = parseInt(data.result, 16)
// Decimal to hex
const hexBlockNumber = "0x" + blockNumber.toString(16)Missing methods in generated tools
Check if the method is:
- In the
excludelist in your config - Marked as deprecated in the OpenRPC spec
- Filtered by
includepatterns
Run with --verbose to see which methods are being filtered:
pets generate-jsonrpc --verboseOpenRPC Spec Resources
Finding OpenRPC Specs
Common locations for blockchain RPC specs:
- Ethereum: https://github.com/etclabscore/ethereum-json-rpc-specification
- Bitcoin: No official OpenRPC spec (would need manual tools)
- Polygon, BSC, Arbitrum: Use Ethereum spec (EVM-compatible)
- Solana: https://solana.com/docs/rpc (no OpenRPC spec yet)
Creating Your Own OpenRPC Spec
If your RPC service doesn't have an OpenRPC spec, you can create one:
- Follow the OpenRPC specification: https://spec.open-rpc.org/
- Use the OpenRPC playground: https://playground.open-rpc.org/
- Generate from your code using openrpc-generator tools
Example minimal OpenRPC spec:
{
"openrpc": "1.0.0",
"info": {
"title": "My RPC Service",
"version": "1.0.0"
},
"servers": [
{
"url": "https://rpc.example.com"
}
],
"methods": [
{
"name": "ping",
"summary": "Test connection",
"params": [],
"result": {
"name": "pong",
"schema": {
"type": "string"
}
}
}
]
}Comparing Generation Methods
| Feature | JSON-RPC | OpenAPI | MCP |
|---|---|---|---|
| Source | OpenRPC spec | OpenAPI/Swagger spec | MCP server |
| Protocol | JSON-RPC 2.0 | REST (HTTP verbs) | MCP protocol |
| Endpoints | Single POST endpoint | Multiple endpoints | MCP transport |
| Auth | Usually none or simple | Token/OAuth | OAuth or token |
| Method naming | RPC method name | HTTP verb + path | Tool name |
| Best for | Blockchain/RPC services | REST APIs | MCP-enabled services |
Next Steps
After generating your JSON-RPC tools:
- Test Connection - Verify the RPC endpoint is accessible
- Add Custom Tools - Supplement generated tools with pet-specific helpers
- Document Usage - Update README with query examples
- Test Queries - Use
bun test:petto validate tool behavior - Publish - Share your pet via
pets publish
For more examples, see:
pets/ethereum-publicnode/- Complete Ethereum RPC implementationdocs/pet-creator.md- General pet creation guidedocs/openapi-generation.md- Comparison with OpenAPI approach