Skip to content

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-jsonrpc CLI command with the @src/core/jsonrpc-generator.ts tool. 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:

  1. Ensures Consistency - All JSON-RPC pets use the same generation patterns
  2. Proper RPC Handling - Handles JSON-RPC 2.0 request/response protocol correctly
  3. OpenPets Integration - Includes read-only mode, error handling, logging
  4. Schema Mapping - Converts OpenRPC schemas to Zod types
  5. 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:

bash
# 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.ts implementation
  • Reads your package.json jsonrpcSpec configuration
  • Generates proper OpenPets-compatible tools
  • Creates jsonrpc-client.ts with 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.json with jsonrpcSpec configuration
  • Updated index.ts to import generated tools

Generated Tools:

  1. ethereum-publicnode-eth-blocknumber - Get current block number
  2. ethereum-publicnode-eth-getbalance - Get account balance
  3. ethereum-publicnode-eth-call - Call smart contract (read-only)
  4. ethereum-publicnode-net-version - Get network ID
  5. ...and 41 more Ethereum JSON-RPC methods

Generation Process:

bash
cd pets/ethereum-publicnode
pets generate-jsonrpc --verbose
 Generated 45 tools (31 read-only, 14 write) to jsonrpc-client.ts

Key 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

ApproachBest For
JSON-RPC GenerationBlockchain nodes, RPC APIs with OpenRPC specs
OpenAPI GenerationREST APIs with OpenAPI specs, API-first products
MCP GenerationServices with official MCP servers
Manual ToolsCustom logic, non-standard protocols

JSON-RPC vs REST

FeatureJSON-RPCREST
EndpointsSingle endpoint (POST)Multiple endpoints (GET/POST/PUT/DELETE)
MethodIn request body (method field)In HTTP verb + URL path
Spec FormatOpenRPCOpenAPI/Swagger
Common UseBlockchain, distributed systemsWeb APIs, SaaS platforms
ParametersArray in params fieldURL params + request body

Quick Start

Option 1: From Command Line

bash
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 --verbose

Add a jsonrpcSpec section to your pet's package.json:

json
{
  "name": "@openpets/my-rpc-service",
  "jsonrpcSpec": {
    "url": "https://example.com/openrpc.json",
    "baseUrl": "https://rpc.example.com"
  }
}

Then run:

bash
cd pets/my-rpc-service
pets generate-jsonrpc

Best Practice: Always use url when 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

PropertyTypeRequiredDescription
urlstringYes*URL to fetch OpenRPC spec from
filestringYes*Local file path to OpenRPC spec
baseUrlstringYesRPC endpoint URL (e.g., https://ethereum-rpc.publicnode.com)
authEnvVarstringNoEnvironment variable for auth token/key
authTypestringNoAuthentication type: bearer, apiKey, basic, or none (default)
includestring[]NoOnly generate tools matching these patterns
excludestring[]NoSkip tools matching these patterns
readOnlyPatternsstring[]NoMark tools matching these patterns as read-only
nameCleaningPatternsstring[]NoRegex patterns to remove from tool names

*Either url or file is required.

CLI Options

bash
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 output

Generated File Structure

The generator creates a jsonrpc-client.ts file with:

  1. Configuration - Base URL and auth from environment
  2. Read-only mode support - Filters write operations when enabled
  3. RPC wrapper - JSON-RPC 2.0 request handler with error parsing
  4. 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

typescript
{
  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:

typescript
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:

typescript
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 MyRpcServicePlugin

Schema Type Mapping

The generator maps OpenRPC/JSON Schema types to Zod types:

OpenRPC TypeZod Type
stringz.string()
string with enumz.enum([...])
number, integerz.number()
booleanz.boolean()
array of stringsz.array(z.string())
array of numbersz.array(z.number())
array of objectsz.string() (JSON string)
object (nested)z.any() with description
anyOf with nullBase type with .optional()

Handling Complex Types

Due to OpenPets runtime schema limits, complex nested structures are serialized as JSON strings:

typescript
// 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:

json
{
  "name": "eth_getBalance",
  "params": [
    {
      "name": "address",
      "schema": { "type": "string" }
    },
    {
      "name": "blockNumber",
      "schema": { "type": "string" }
    }
  ]
}

Generated Tool:

typescript
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)

json
{
  "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

json
{
  "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:

typescript
headers["Authorization"] = `Bearer ${apiKey}`

API Key Header

json
{
  "jsonrpcSpec": {
    "url": "https://example.com/openrpc.json",
    "baseUrl": "https://rpc.example.com",
    "authType": "apiKey",
    "apiKeyName": "X-API-Key"
  }
}

Basic Auth

json
{
  "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:

bash
pets read-only my-rpc-service on

Write operations are filtered out based on read-only patterns.

Default read-only patterns: get, list, eth_, net_, web3_

Custom patterns:

json
{
  "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

json
{
  "jsonrpcSpec": {
    "url": "https://example.com/openrpc.json",
    "baseUrl": "https://rpc.example.com",
    "include": ["eth_getBalance", "eth_blockNumber", "eth_call"]
  }
}

Exclude Methods

json
{
  "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:

bash
cd pets/my-rpc-service
pets generate-jsonrpc

The 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:

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:

bash
cd pets/ethereum-publicnode
pets generate-jsonrpc --verbose

Output:

Generated 45 tools to jsonrpc-client.ts
  31 read-only (eth_getBalance, eth_blockNumber, eth_call, etc.)
  14 write (excluded by config)

Generated Tools:

CategoryMethods
Block Querieseth-blocknumber, eth-getblockbyhash, eth-getblockbynumber
Account/Balanceeth-getbalance, eth-gettransactioncount, eth-getcode
Transactionseth-gettransactionbyhash, eth-gettransactionreceipt
Smart Contractseth-call, eth-estimategas
Network Infoeth-gasprice, net-version, web3-clientversion

Usage Examples:

bash
# 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

  1. No Authentication Required: PublicNode is completely free, making it ideal for read-only blockchain queries.

  2. Hex Number Handling: Ethereum returns numbers as hex strings (e.g., 0x173d76b). The AI assistant can convert these or display them in both formats.

  3. Read-Only by Default: Excluding eth_sendRawTransaction and eth_sendTransaction prevents accidental write operations on mainnet.

  4. OpenRPC Spec Quality: The ethereum-json-rpc-specification project provides a well-maintained OpenRPC spec that covers all standard Ethereum JSON-RPC methods.

  5. Block Parameter Types: Methods like eth_getBalance accept block parameters that can be:

    • Block number (hex string like 0x1234)
    • Block tags (latest, earliest, pending)
    • Block hash
  6. Response Consistency: All methods return structured JSON with success and result fields, 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:

json
{
  "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:

typescript
// 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:

  1. HTTP Errors (connection failed, 404, 500, etc.)
  2. RPC Errors (method not found, invalid params, etc.)

Both are returned as structured JSON:

json
{
  "success": false,
  "error": "Method not found",
  "code": -32601
}

Hex number conversion

Many blockchain RPC methods return hex-encoded numbers. You can convert them:

typescript
// 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 exclude list in your config
  • Marked as deprecated in the OpenRPC spec
  • Filtered by include patterns

Run with --verbose to see which methods are being filtered:

bash
pets generate-jsonrpc --verbose

OpenRPC Spec Resources

Finding OpenRPC Specs

Common locations for blockchain RPC specs:

Creating Your Own OpenRPC Spec

If your RPC service doesn't have an OpenRPC spec, you can create one:

  1. Follow the OpenRPC specification: https://spec.open-rpc.org/
  2. Use the OpenRPC playground: https://playground.open-rpc.org/
  3. Generate from your code using openrpc-generator tools

Example minimal OpenRPC spec:

json
{
  "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

FeatureJSON-RPCOpenAPIMCP
SourceOpenRPC specOpenAPI/Swagger specMCP server
ProtocolJSON-RPC 2.0REST (HTTP verbs)MCP protocol
EndpointsSingle POST endpointMultiple endpointsMCP transport
AuthUsually none or simpleToken/OAuthOAuth or token
Method namingRPC method nameHTTP verb + pathTool name
Best forBlockchain/RPC servicesREST APIsMCP-enabled services

Next Steps

After generating your JSON-RPC tools:

  1. Test Connection - Verify the RPC endpoint is accessible
  2. Add Custom Tools - Supplement generated tools with pet-specific helpers
  3. Document Usage - Update README with query examples
  4. Test Queries - Use bun test:pet to validate tool behavior
  5. Publish - Share your pet via pets publish

For more examples, see:

  • pets/ethereum-publicnode/ - Complete Ethereum RPC implementation
  • docs/pet-creator.md - General pet creation guide
  • docs/openapi-generation.md - Comparison with OpenAPI approach