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