Skip to content

SDK Tool Generation

This guide explains how to automatically generate OpenPets-compatible tools from TypeScript SDK packages, rather than manually defining each tool.

Overview

Many services provide official TypeScript SDKs (like @polar-sh/sdk, @stripe/stripe-node, etc.) that expose their APIs as typed methods. Instead of manually writing tool definitions for each API endpoint, you can use bun scripts/generate-from-sdk.ts --pet <pet-name> to generate tools from these SDKs.

This approach is similar to how hey-api generates TypeScript clients from OpenAPI specs - you configure the SDK, and tools are generated automatically.

Note: There is no pets generate-sdk CLI command registered in the pets CLI. Use the script directly with bun scripts/generate-from-sdk.ts --pet <pet-name> from the repository root.

When to Use SDK Generation

Use SDK tool generation when:

  • The service provides an official TypeScript SDK
  • You want to quickly bootstrap a pet with many tools
  • The SDK is well-maintained with proper TypeScript types
  • You prefer direct SDK integration over MCP servers

Consider MCP generation or manual tool definitions when:

  • The service provides an MCP server (may have better authentication)
  • You need custom logic beyond what the SDK provides
  • You want more control over tool behavior
  • The SDK doesn't follow predictable patterns

How It Works

The SDK generator:

  1. Reads configuration from package.json or sdk-config.json
  2. Generates tool definitions for each SDK method
  3. Creates a client.ts file with typed tool exports
  4. Supports read-only mode filtering

Step-by-Step Guide

Step 1: Add SDK Generator Config

Add an sdkGenerator section to your pet's package.json:

json
{
  "name": "@openpets/my-service",
  "sdkGenerator": {
    "sdkPackage": "@my-service/sdk",
    "petName": "my-service",
    "mainClassName": "MyService",
    "tokenEnvVar": "MY_SERVICE_TOKEN",
    "description": "My Service SDK integration",
    "namespaces": [
      {
        "name": "users",
        "methods": ["list", "get", "create", "update", "delete"],
        "readOnlyMethods": ["list", "get"]
      },
      {
        "name": "projects",
        "methods": ["list", "get", "create"],
        "readOnlyMethods": ["list", "get"]
      }
    ]
  }
}

Or create a separate sdk-config.json file:

json
{
  "sdkPackage": "@my-service/sdk",
  "petName": "my-service",
  "mainClassName": "MyService",
  "tokenEnvVar": "MY_SERVICE_TOKEN",
  "description": "My Service SDK integration",
  "namespaces": [
    {
      "name": "users",
      "methods": ["list", "get", "create", "update", "delete"],
      "readOnlyMethods": ["list", "get"]
    }
  ]
}

Step 2: Set Up Authentication

Set the access token environment variable in your .env file:

bash
MY_SERVICE_TOKEN=your-token-here

Step 3: Generate Tools

Run the SDK generator from the repository root:

bash
# Preview what will be generated
bun scripts/generate-from-sdk.ts --pet my-service --dry-run --verbose

# Generate the client.ts file
bun scripts/generate-from-sdk.ts --pet my-service

You'll see output like:

SDK Package: @my-service/sdk
Pet Name: my-service
Main Class: MyService
Namespaces: 2

Generated 8 tool definitions from 2 namespaces

Tools:
  [read] my-service-users-list
  [read] my-service-users-get
  [write] my-service-users-create
  [write] my-service-users-update
  [write] my-service-users-delete
  [read] my-service-projects-list
  [read] my-service-projects-get
  [write] my-service-projects-create

Generated 8 tools (5 read-only, 3 write) to client.ts

Step 4: Import SDK Tools in Your Plugin

Update your index.ts to import and include the generated tools:

typescript
import { z, createPlugin, type ToolDefinition, createLogger, loadEnv } from "openpets-sdk"
import { createMyServiceTools } from "./client"

export const MyServicePlugin = async () => {
  const logger = createLogger("my-service")
  const env = loadEnv("my-service")
  
  const ACCESS_TOKEN = env.MY_SERVICE_TOKEN
  const isConfigured = !!ACCESS_TOKEN
  
  if (!isConfigured) {
    logger.warn("Plugin not configured - returning limited toolset")
    return createPlugin([
      {
        name: "my-service-test-connection",
        description: "Test connection status",
        schema: z.object({}),
        async execute() {
          return JSON.stringify({
            success: false,
            status: "not_configured",
            message: "Please set MY_SERVICE_TOKEN environment variable."
          }, null, 2)
        }
      }
    ])
  }
  
  // Create SDK tools with the access token
  const sdkTools = createMyServiceTools({ accessToken: ACCESS_TOKEN })
  
  return createPlugin(sdkTools)
}

export default MyServicePlugin

Step 5: Test the Integration

bash
# Test the connection
bun test:pet my-service --query "test connection"

Configuration Reference

SDKGeneratorConfig

PropertyTypeRequiredDescription
sdkPackagestringYesNPM package name (e.g., "@polar-sh/sdk")
petNamestringYesPet name for tool naming (e.g., "polar")
mainClassNamestringYesMain class exported by SDK (e.g., "Polar")
tokenEnvVarstringYesEnv var name for the token (e.g., "POLAR_TOKEN")
descriptionstringNoDescription of the SDK
orgIdEnvVarstringNoOptional org ID env var
namespacesarrayYesArray of namespace configurations

SDKNamespaceConfig

PropertyTypeRequiredDescription
namestringYesNamespace name (e.g., "products", "users")
methodsstring[]YesMethod names to generate tools for
readOnlyMethodsstring[]NoMethods that are safe in read-only mode

Method Name Patterns

The generator automatically creates appropriate schemas based on method names:

PatternSchema GeneratedDescription
list, list*{ organizationId?, limit? }Paginated listing
get, get*{ id }Get by ID
create{ dataJson }Create with JSON data
update, update*{ id, dataJson }Update by ID
delete, revoke{ id }Delete by ID
validate{ key, organizationId }Validate a key
export{}Export data
Other{}Empty schema

Generated File Structure

The client.ts file includes:

  1. SDK Client Initialization - Creates the SDK client with access token
  2. Read-Only Mode Support - Filters write tools when read-only is enabled
  3. Tool Definitions - Auto-generated ToolDefinition array with:
    • Kebab-case names prefixed with pet name (e.g., polar-products-list)
    • Descriptions based on method patterns
    • Zod schemas for input validation
    • Execute functions that call SDK methods

Example generated tool:

typescript
{
  name: "polar-products-get",
  description: "Get a product by ID",
  schema: z.object({
    id: z.string().describe("Product ID")
  }),
  async execute(args) {
    try {
      const result = await client.products.get(args)
      return JSON.stringify(result, null, 2)
    } catch (error: any) {
      return JSON.stringify({ success: false, error: error.message }, null, 2)
    }
  }
}

Handling Paginated Methods

List methods automatically iterate through all pages:

typescript
async execute(args) {
  try {
    const items: any[] = []
    const result = await client.products.list(args)
    for await (const page of result) {
      if (page?.items) items.push(...page.items)
      else if (page?.result?.items) items.push(...page.result.items)
      else if (Array.isArray(page)) items.push(...page)
    }
    return JSON.stringify({ total: items.length, items }, null, 2)
  } catch (error: any) {
    return JSON.stringify({ success: false, error: error.message }, null, 2)
  }
}

Read-Only Mode and Tool Classification

The SDK generator implements a two-layer approach to classify tools as read-only or write operations:

Layer 1: Configuration-Based Classification

When defining namespaces, you explicitly mark which methods are read-only:

json
{
  "namespaces": [
    {
      "name": "products",
      "methods": ["list", "get", "create", "update", "delete"],
      "readOnlyMethods": ["list", "get"]
    }
  ]
}

This generates a READ_ONLY_TOOLS array in the client:

typescript
// Read-only tool names (safe to use in read-only mode)
const READ_ONLY_TOOLS = [
  "polar-products-list",
  "polar-products-get",
  "polar-customers-list",
  "polar-customers-get",
  // ... all tools from readOnlyMethods
]

Layer 2: Pattern-Based Filtering

At runtime, the filterToolsForReadOnly function filters tools based on name patterns:

typescript
// Write operation patterns for filtering
const WRITE_PATTERNS = ['create', 'update', 'delete', 'add', 'remove', 'revoke', 'archive']

// Filter applied when read-only mode is enabled
const tools = filterToolsForReadOnly(allTools, WRITE_PATTERNS, readOnlyMode, logger)

How Read-Only Mode Gets Enabled

Users can enable read-only mode in three ways (priority order):

  1. Environment variable (highest priority):

    bash
    export POLAR_READ_ONLY=true
  2. CLI command:

    bash
    pets read-only polar on
  3. Config file (.pets/config.json):

    json
    {
      "petConfig": {
        "polar": { "readOnly": true }
      }
    }

Runtime Behavior

When a user runs with read-only mode enabled:

$ pets read-only polar on
$ bun test:pet polar --query "list polar products"

# The plugin logs:
[polar-client] READ-ONLY MODE - write operations disabled

# Only read tools are available:
# ✅ polar-products-list
# ✅ polar-products-get
# ❌ polar-products-create (filtered out)
# ❌ polar-products-update (filtered out)
# ❌ polar-products-delete (filtered out)

Verbose Dry-Run Output

The generator shows tool classification during generation:

$ bun scripts/generate-from-sdk.ts --pet my-service --dry-run --verbose

Tools:
  [read] polar-products-list
  [read] polar-products-get
  [write] polar-products-create
  [write] polar-products-update
  [write] polar-products-delete

Generated 88 tools (45 read-only, 43 write) to client.ts

Real-World Example: Polar SDK

The Polar SDK is a built-in example. With @polar-sh/sdk as a dependency, the generator auto-detects it:

json
{
  "name": "@openpets/polar",
  "dependencies": {
    "@polar-sh/sdk": "latest"
  }
}

Run generation:

bash
# Run from repository root
bun scripts/generate-from-sdk.ts --pet polar --verbose

This generates 88 tools (45 read-only, 43 write) covering:

  • Organizations (list, get, create, update)
  • Products (list, get, create, update, updateBenefits)
  • Customers (list, get, create, update, delete, getExternal, updateExternal, deleteExternal, getState, getStateExternal)
  • Orders (list, get, update, generateInvoice, invoice, export)
  • Subscriptions (list, get, create, update, revoke, export)
  • Benefits (list, get, create, update, delete, grants)
  • Checkouts (list, get, create, update)
  • Checkout Links (list, get, create, update)
  • Discounts (list, get, create, update, delete)
  • License Keys (list, get, create, update, getActivation, validate)
  • Metrics (get, limits)
  • Webhooks (listWebhookEndpoints, getWebhookEndpoint, createWebhookEndpoint, updateWebhookEndpoint, deleteWebhookEndpoint, listWebhookDeliveries)
  • Files (list, create, update, delete, upload)
  • Payments (list, get)
  • Refunds (list)
  • Meters (list, get, create, update, quantities)
  • Customer Meters (list, get)
  • Custom Fields (list, get, create, update, delete)

CLI Reference

bash
# Basic usage (from repository root)
bun scripts/generate-from-sdk.ts --pet my-service

# Custom output file
bun scripts/generate-from-sdk.ts --pet my-service --output pets/my-service/sdk-tools.ts

# Preview without writing
bun scripts/generate-from-sdk.ts --pet my-service --dry-run

# Verbose output showing generated tools
bun scripts/generate-from-sdk.ts --pet my-service --verbose

# Combined
bun scripts/generate-from-sdk.ts --pet my-service --dry-run --verbose

Regenerating Tools

When the SDK updates with new methods:

bash
# Run from repository root
bun scripts/generate-from-sdk.ts --pet my-service

The client.ts file will be regenerated with the latest tools.

Troubleshooting

"No sdkGenerator configuration found"

Add an sdkGenerator section to your package.json or create an sdk-config.json file.

"Cannot find module"

Install the SDK package:

bash
cd pets/my-service
bun add @my-service/sdk

Generated tools have wrong types

Check that your namespaces configuration matches the SDK's actual namespace and method names. Look at the SDK's README or TypeScript definitions for the correct names.

Paginated methods not collecting all items

The generator tries multiple patterns for pagination. If your SDK uses a different pattern, you may need to customize the generated code.

Comparing SDK vs MCP Generation

AspectSDK GenerationMCP Generation
SourceTypeScript SDK packageMCP server (local or remote)
AuthenticationEnvironment variable tokenToken or OAuth (browser)
SpeedVery fast (no network)Slower (connects to server)
Schema accuracyBased on config patternsExtracted from MCP server
CustomizationEdit config, regenerateLimited to MCP server tools
DependenciesSDK package onlyMCP client + server package

Adding Support for New SDKs

To add built-in support for a new SDK, edit src/core/sdk-generator.ts and add a new config function similar to createPolarSDKConfig().

For SDKs not built-in, create an sdk-config.json file with the full configuration.