Skip to content

OpenAPI Tool Generation

This guide explains how to automatically generate OpenPets-compatible tools from OpenAPI/Swagger specifications using the dedicated OpenAPI generator in src/core/openapi-generator.ts.

CRITICAL: ALL tools for OpenAPI-based pets must be generated using the pets generate-openapi CLI command with the @src/core/openapi-generator.ts tool. This ensures consistency, proper schema handling, and compatibility with OpenPets standards.

Why Use the Official Generator

The @src/core/openapi-generator.ts is the canonical way to generate OpenAPI tools because it:

  1. Ensures Consistency - All OpenAPI pets use the same generation patterns
  2. Proper Schema Mapping - Handles OpenAPI → Zod type conversion correctly
  3. OpenPets Integration - Includes read-only mode, core endpoints, auth handling
  4. Error Handling - Standardized error responses and fetch wrapper
  5. Future-Proof - Updates to generator benefit all OpenAPI pets

REQUIRED: Use pets generate-openapi CLI

ALWAYS generate OpenAPI tools using the official CLI:

bash
# NEVER write tools manually for OpenAPI specs
cd pets/my-api
pets generate-openapi

# NOT this:
# ❌ Manual tool writing (outdated approach)
# ❌ Custom scripts (may break compatibility)
# ❌ Other generators (lack OpenPets features)

The CLI automatically:

  • Uses the @src/core/openapi-generator.ts implementation
  • Reads your package.json openapiSpec configuration
  • Generates proper OpenPets-compatible tools
  • Creates openapi-client.ts (utilities) + openapi-tools.ts (tool definitions)
  • Handles authentication, read-only mode, and core endpoints
  • Applies TOON format by default for 23.6% token reduction (learn more)

Working Example: Commander Pet

The pets/commander pet demonstrates the complete OpenAPI generation workflow:

Files Created:

  • openapi-spec.json - Complete OpenAPI 3.0.3 specification
  • openapi-client.ts - Auto-generated tools (200+ lines)
  • Updated package.json with openapiSpec configuration
  • Updated index.ts to import generated tools

Generated Tools:

  1. commander-test-connection - Test API connection
  2. commander-list-items - List items with filtering
  3. commander-get-item - Get item by ID
  4. commander-create-item - Create new item

Generation Process:

bash
cd pets/commander
pets generate-openapi --verbose
 Generated 4 tools (3 read-only, 1 write) to openapi-client.ts

Key Features Demonstrated:

  • Bearer token authentication with TEMPLATE_API_KEY
  • Request body parameter expansion
  • Read-only mode support
  • Proper error handling
  • Core endpoints configuration

This example shows the complete, production-ready approach for OpenAPI-based pets.

Overview

Many modern APIs provide OpenAPI (formerly Swagger) specifications that describe their endpoints, parameters, and data models. Instead of manually writing tool definitions for each endpoint, you can use the pets generate-openapi command to automatically generate tools from these specs.

This approach is ideal for:

  • REST APIs with OpenAPI 3.0/3.1 documentation
  • API-first products like SaaS platforms
  • Quickly bootstrapping a pet with many endpoints
  • Keeping tools in sync with evolving APIs

When to Use OpenAPI Generation

ApproachBest For
OpenAPI GenerationREST APIs with OpenAPI specs, API-first products
MCP GenerationServices with official MCP servers (Asana, Atlassian, etc.)
SDK GenerationServices with TypeScript SDKs (@polar-sh/sdk, etc.)
Manual ToolsCustom logic, non-standard APIs, complex workflows

Quick Start

Option 1: From Command Line

bash
cd pets/my-api

# Generate from URL (PREFERRED - always gets latest spec)
pets generate-openapi --url https://api.example.com/openapi.json

# Generate from local file (only if URL not available)
pets generate-openapi --file ./openapi.json

# Preview without writing
pets generate-openapi --url https://api.example.com/openapi.json --dry-run --verbose

Optional Include-First Scaffolding for Large Specs

By default, OpenAPI generation loads the full generated tool surface. Use include-first only when you deliberately want staged loading or a smaller initial surface.

When a spec has many endpoint groups, start with a smaller primary set:

bash
pets new my-large-api --type openapi --url https://api.example.com/openapi.json --include-first

What this does:

  • Adds openapiSpec.include with inferred core endpoint groups (tag-based when possible, path-based fallback).
  • Adds tier defaults so deferred groups are marked as secondary for future expansion.
  • Keeps full expansion simple: remove openapiSpec.include and run pets generate-openapi again.

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

json
{
  "name": "@openpets/my-api",
  "openapiSpec": {
    "url": "https://api.example.com/openapi.json",
    "baseUrl": "https://api.example.com"
  }
}

Then run:

bash
cd pets/my-api
pets generate-openapi

Best Practice: Always use url when the API provider hosts their OpenAPI spec. This ensures:

  • Tools are generated from the latest spec version
  • New endpoints are available after regenerating
  • Deprecated endpoints are automatically removed
  • No need to manually sync local files

Configuration Reference

package.json openapiSpec

PropertyTypeRequiredDescription
urlstringYes*URL to fetch OpenAPI spec from
filestringYes*Local file path to OpenAPI spec
baseUrlstringNoBase URL for API calls (overrides spec's servers[0].url)
altBaseUrlstringNoAlternative base URL for environment switching (e.g., live vs demo)
baseUrlEnvVarstringNoEnvironment variable to select base URL (e.g., MY_API_ENVIRONMENT)
baseUrlEnvValuestringNoValue that triggers altBaseUrl (default: "live")
authEnvVarstringNoEnvironment variable for auth token/key (default: {PET_NAME}_API_KEY)
authSecretEnvVarstringNoEnvironment variable for auth secret (used with Basic auth)
authTypestringNoAuthentication type: bearer, apiKey, basic, or none
apiKeyHeaderstringNoHeader name for API key auth (default: X-API-Key)
apiKeyInstringNoAPI key location: header, query, or cookie
apiKeyNamestringNoAPI key header/query parameter name. Prefer this over apiKeyHeader
apiKeyValuePrefixstringNoOptional prefix for API key values, e.g. ShippoToken for Authorization header auth
responseFormatstringNoResponse format: toon (default, 23.6% token savings), json, or auto - See details
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 (e.g., ["-controller", "-\\d{4}-\\d{2}-\\d{2}"])
coreEndpointsobjectNoConfigure which tools are core (always loaded) vs loadEnv (require env var)
coreEndpoints.tagsstring[]NoTags whose tools should always be loaded
coreEndpoints.pathsstring[]NoPath patterns whose tools should always be loaded
coreEndpoints.operationIdsstring[]NoOperation IDs whose tools should always be loaded

*Either url or file is required.

CLI Options

bash
pets generate-openapi [options]

Options:
  --url <url>           URL to fetch OpenAPI spec from
  --file <path>         Local file path to OpenAPI spec  
  --base-url <url>      Base URL for API calls (overrides spec servers)
  --output <file>       Output file name (default: openapi-client.ts)
  --dry-run, -n         Preview without writing files
  --verbose, -v         Show detailed output
  --dump-spec           Dump raw spec to JSON for debugging

Generated File Structure

The generator creates an openapi-client.ts file with:

  1. Configuration - Base URL and auth token from environment
  2. Read-only mode support - Filters write operations when enabled
  3. Fetch wrapper - Error handling and response parsing
  4. Tool definitions - One tool per endpoint with:
    • Kebab-case name with pet prefix
    • Description from operation summary/description
    • Zod schema with all parameters
    • Execute function that calls the API

Example Generated Tool

typescript
{
  name: "my-api-create-user",
  description: "Create a new user",
  schema: z.object({
    email: z.string().describe("User email address"),
    name: z.string().describe("User display name"),
    role: z.enum(["admin", "user"]).describe("User role").optional()
  }),
  async execute(args: any) {
    const url = `${baseUrl}/api/v1/users`
    const fullUrl = url
    
    const options: RequestInit = {
      method: "POST",
      headers: { ...headers },
      body: JSON.stringify({
        ...(args.email !== undefined ? { email: args.email } : {}),
        ...(args.name !== undefined ? { name: args.name } : {}),
        ...(args.role !== undefined ? { role: args.role } : {})
      })
    }
    
    return fetchAPI(fullUrl, options)
  }
}

Lazy Authentication

The generated fetchAPI function uses lazy authentication - it reads environment variables at request time, not at module load time. This ensures that .env files loaded after the module is imported are still picked up:

typescript
/**
 * Get auth headers lazily - reads env vars at request time
 */
function getAuthHeaders(): Record<string, string> {
  const headers = { "Content-Type": "application/json", "Accept": "application/json" }
  
  // Read env vars fresh on each request
  const apiKey = process.env.MY_API_KEY
  if (apiKey) {
    headers["Authorization"] = `Bearer ${apiKey}`
  }
  
  return headers
}

async function fetchAPI(url: string, options: RequestInit): Promise<string> {
  // Get fresh auth headers on each request
  const authHeaders = getAuthHeaders()
  const response = await fetch(url, { ...options, headers: authHeaders })
  // ...
}

This pattern solves a common issue where the runtime loads .env files after modules are imported, causing auth headers to be built with undefined values.

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 openAPITools from "./openapi-client"

loadEnv()

const logger = createLogger("my-api")

export const MyApiPlugin = async () => {
  const API_KEY = process.env.MY_API_API_KEY
  const isConfigured = !!API_KEY

  // Custom tools
  const customTools: ToolDefinition[] = [
    {
      name: "my-api-test-connection",
      description: "Test API connection",
      schema: z.object({}),
      async execute() {
        try {
          const response = await fetch("https://api.example.com/health")
          const data = await response.json()
          return JSON.stringify({
            success: true,
            status: "connected",
            configured: isConfigured
          }, null, 2)
        } catch (error: any) {
          return JSON.stringify({
            success: false,
            error: error.message
          }, null, 2)
        }
      }
    }
  ]

  if (!isConfigured) {
    logger.warn("API key not set - returning limited toolset")
    return createPlugin(customTools)
  }

  // Combine custom tools with OpenAPI-generated tools
  return createPlugin([
    ...customTools,
    ...openAPITools
  ])
}

export default MyApiPlugin

Schema Type Mapping

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

OpenAPI TypeZod Type
stringz.string()
string with enumz.enum([...])
string with format: emailz.string().email()
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.string() (JSON string 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 with descriptive documentation:

typescript
// Nested object becomes:
nestedData: z.string().describe("JSON object: Properties: id (required): string; config: object with: {enabled, timeout}")

// Array of objects becomes:
items: z.string().describe("JSON array of objects")

Request Body Expansion

Unlike other generators that use a single body parameter, the OpenAPI generator expands request body properties into individual parameters:

OpenAPI Spec:

yaml
requestBody:
  content:
    application/json:
      schema:
        type: object
        required: [email]
        properties:
          email:
            type: string
          name:
            type: string

Generated Schema:

typescript
schema: z.object({
  email: z.string().describe("Email"),
  name: z.string().describe("Name").optional()
})

This makes it easier for AI assistants to understand and use individual parameters.

Authentication

The generator supports multiple authentication methods:

Bearer Token (Default)

json
{
  "openapiSpec": {
    "url": "https://api.example.com/openapi.json",
    "authType": "bearer"
  }
}

Set MY_API_API_KEY in your .env file. The generator adds:

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

API Key Header

json
{
  "openapiSpec": {
    "url": "https://api.example.com/openapi.json",
    "authType": "apiKey",
    "apiKeyName": "X-API-Key"
  }
}

For APIs that need a value prefix in the API key header, such as Authorization: ShippoToken <token>:

json
{
  "openapiSpec": {
    "url": "https://api.example.com/openapi.json",
    "authType": "apiKey",
    "apiKeyIn": "header",
    "apiKeyName": "Authorization",
    "apiKeyValuePrefix": "ShippoToken ",
    "authEnvVar": "SERVICE_API_TOKEN"
  }
}

Basic Auth (Single Variable)

For APIs that expect a pre-encoded Basic auth token:

json
{
  "openapiSpec": {
    "url": "https://api.example.com/openapi.json",
    "authType": "basic"
  }
}

The generator expects MY_API_API_KEY to contain the base64-encoded credentials.

Basic Auth (Key + Secret)

For APIs that require separate API key and secret (like Trading 212):

json
{
  "openapiSpec": {
    "url": "https://api.example.com/openapi.json",
    "authType": "basic",
    "authEnvVar": "MY_API_API_KEY",
    "authSecretEnvVar": "MY_API_API_SECRET"
  }
}

This generates proper Basic auth encoding at runtime:

typescript
const apiKey = process.env.MY_API_API_KEY
const apiSecret = process.env.MY_API_API_SECRET

function getBasicAuthHeader(): string | null {
  if (!apiKey || !apiSecret) return null
  const credentials = `${apiKey}:${apiSecret}`
  const encoded = Buffer.from(credentials).toString("base64")
  return `Basic ${encoded}`
}

No Authentication

json
{
  "openapiSpec": {
    "url": "https://api.example.com/openapi.json",
    "authType": "none"
  }
}

Dynamic Base URL (Environment Switching)

Many APIs provide multiple environments (demo/sandbox vs live/production). The generator supports dynamic base URL selection at runtime based on an environment variable.

Configuration

json
{
  "openapiSpec": {
    "file": "./openapi-spec.json",
    "baseUrl": "https://demo.api.example.com",
    "altBaseUrl": "https://live.api.example.com",
    "baseUrlEnvVar": "MY_API_ENVIRONMENT",
    "baseUrlEnvValue": "live"
  }
}

Generated Code

typescript
const environment = process.env.MY_API_ENVIRONMENT || "demo"
const baseUrl = environment === "live" 
  ? "https://live.api.example.com" 
  : "https://demo.api.example.com"

Usage

bash
# Use demo environment (default)
bun test:pet my-api --query "test connection"

# Use live environment
MY_API_ENVIRONMENT=live bun test:pet my-api --query "test connection"

This is especially useful for:

  • Financial APIs - Paper trading vs real money
  • Payment APIs - Sandbox vs production
  • Testing - Staging vs production environments

Read-Only Mode

Generated tools automatically support read-only mode. When enabled:

bash
pets read-only my-api on

Write operations (POST, PUT, PATCH, DELETE) are filtered out, leaving only read operations (GET, HEAD, OPTIONS).

Default read-only patterns: list, get, search, check, status, health

Custom patterns:

json
{
  "openapiSpec": {
    "url": "https://api.example.com/openapi.json",
    "readOnlyPatterns": ["list", "get", "fetch", "query"]
  }
}

Core Endpoints (Tool Grouping)

For APIs with many endpoints, you can mark the most important endpoints as core. Core and additional groups are both loaded by default; the grouping is used for documentation, ordering, and tier metadata rather than hiding tools behind environment variables.

How It Works

  1. Core tools - Grouped as the most important tools based on coreEndpoints configuration
  2. Additional tools - Generated into named groups and loaded by default

Configuration Example

json
{
  "openapiSpec": {
    "url": "https://api.example.com/openapi.json",
    "coreEndpoints": {
      "tags": ["Generation", "Health"],
      "paths": ["/api/v1/generate", "/api/v1/status"]
    }
  }
}

This configuration:

  • Always loads tools from the "Generation" and "Health" tags
  • Always loads tools whose paths start with /api/v1/generate or /api/v1/status
  • Groups other tools separately for documentation and metadata

Real-World Example: AI Harmony

The AI Harmony pet uses core endpoints to mark audio generation tools as core:

json
{
  "name": "ai-harmony",
  "openapiSpec": {
    "url": "https://ai-harmony.duckdns.org/obsidian/openapi.json",
    "baseUrl": "https://api.obsidian-neural.com",
    "authType": "apiKey",
    "apiKeyHeader": "X-API-Key",
    "coreEndpoints": {
      "tags": ["Generation"],
      "paths": ["/api/v1/generate", "/api/v1/health", "/api/v1/status"]
    }
  }
}

Core tools (always loaded):

  • ai-harmony-generate-audio - Generate audio with AI
  • ai-harmony-generate-audio-test - Test audio generation
  • ai-harmony-health-check - Check API health
  • ai-harmony-check-services-status - Check service status

Additional tools (also loaded by default):

  • Authentication group → login, register, 2FA, subscription tools
  • Admin group → user management, analytics, broadcast tools
  • Gifts group → gift purchase and activation tools

Benefits

  1. Clear organization - Core tools remain easy to identify in large APIs
  2. Predictable availability - Agents can use the full generated API without extra setup
  3. Better metadata - Tiers and groups still help discovery and documentation
  4. Credential safety - Tools that require separate credentials can still be filtered by missing credential env vars

Generated File Structure

With coreEndpoints configured, the generated openapi-tools.ts includes:

typescript
// CORE TOOLS - Always loaded (4 tools)
const coreTools: ToolDefinition[] = [
  // generate-audio, generate-audio-test, health-check, etc.
]

// ADDITIONAL TOOLS - Loaded by default
const aiharmonyloadauthenticationtoolsTools: ToolDefinition[] = [
  // login, register, 2fa tools...
]

const aiharmonyloadadmintoolsTools: ToolDefinition[] = [
  // admin tools...
]

// All tools combined
const allTools: ToolDefinition[] = [
  ...coreTools,
  ...aiharmonyloadauthenticationtoolsTools,
  ...aiharmonyloadadmintoolsTools,
]

Filtering Endpoints

Include Only Specific Endpoints

json
{
  "openapiSpec": {
    "url": "https://api.example.com/openapi.json",
    "include": ["users", "projects"]
  }
}

Exclude Endpoints

json
{
  "openapiSpec": {
    "url": "https://api.example.com/openapi.json",
    "exclude": ["admin", "internal", "deprecated"]
  }
}

Tool Name Cleaning

Many OpenAPI specifications include verbose naming conventions in operation IDs that result in excessively long or redundant tool names. Examples include:

  • Controller suffixes: users-controller-get-users
  • Versioned dates: bookings-controller-2024-08-13-get-bookings
  • API prefixes: api-v1-users-get
  • Service names: user-service-get-user

The nameCleaningPatterns configuration allows you to define regex patterns that are automatically removed from generated tool names, resulting in cleaner, more concise tool names.

Configuration

Add regex patterns to the nameCleaningPatterns array in your package.json:

json
{
  "openapiSpec": {
    "url": "https://api.example.com/openapi.json",
    "nameCleaningPatterns": [
      "-controller",
      "-\\d{4}-\\d{2}-\\d{2}",
      "-service",
      "-api-v\\d+"
    ]
  }
}

Each pattern is applied globally to the generated tool name using JavaScript's RegExp with the g flag.

How It Works

The cleaning algorithm:

  1. Apply patterns - Each regex pattern is removed from the tool name
  2. Normalize separators - Multiple consecutive dashes are collapsed to single dashes
  3. Remove duplicates - Duplicate segments are removed (e.g., cal-bookings-bookings-getcal-bookings-get)
  4. Enforce 64-char limit - If still too long, intelligently truncates to action + resource

Real-World Example: Cal.com

The Cal.com API uses operation IDs like BookingsController_2024-08-13_getBookings, which would generate tool names exceeding the runtime's 64-character limit.

Configuration:

json
{
  "name": "@openpets/cal",
  "openapiSpec": {
    "url": "https://raw.githubusercontent.com/calcom/cal.com/main/docs/api-reference/v2/openapi.json",
    "nameCleaningPatterns": [
      "-controller",
      "-\\d{4}-\\d{2}-\\d{2}"
    ]
  }
}

Before cleaning:

Original Tool NameLength
cal-bookings-controller-2024-08-13-get-bookings49 chars
cal-event-types-controller-2024-06-14-get-event-types55 chars
cal-organizations-attributes-options-controller-get-organization-attribute-assigned-options-by-slug99 chars ⚠️

After cleaning:

Cleaned Tool NameLength
cal-bookings-get16 chars ✅
cal-event-types-get20 chars ✅
cal-org-teams-verified-resources-request-phone-verification-code64 chars ✅

All 267 generated tools now fit within the runtime's 64-character limit.

Common Patterns

Here are regex patterns commonly used across different API styles:

json
{
  "nameCleaningPatterns": [
    "-controller",           // Spring/NestJS controllers
    "-service",              // Microservice naming
    "-\\d{4}-\\d{2}-\\d{2}", // ISO date stamps (YYYY-MM-DD)
    "-v\\d+",                // Version numbers (-v1, -v2)
    "-api",                  // API suffix
    "-endpoint",             // Endpoint suffix
    "^api-",                 // API prefix
    "-handler",              // Handler suffix
    "-resource"              // REST resource suffix
  ]
}

Escaping Special Characters

When using regex special characters in patterns, escape them with double backslashes:

json
{
  "nameCleaningPatterns": [
    "-\\d{4}-\\d{2}-\\d{2}",  // Digits: \d becomes \\d in JSON
    "-v\\d+",                 // One or more digits
    "\\(deprecated\\)",       // Literal parentheses
    "-\\[beta\\]"             // Literal brackets
  ]
}

Debugging Name Cleaning

Use the --verbose flag to see how patterns are applied:

bash
cd pets/my-api
pets generate-openapi --verbose

Output includes before/after tool names:

Processing: users-controller-2024-08-13-get-users
  - Applied pattern: -controller → users-2024-08-13-get-users
  - Applied pattern: -\d{4}-\d{2}-\d{2} → users-get-users
  - Removed duplicates → users-get
✅ Generated tool: my-api-users-get (17 chars)

Benefits

  1. Fits within limits - All tools stay under the runtime's 64-character limit
  2. Improved readability - cal-bookings-get is clearer than cal-bookings-controller-2024-08-13-get-bookings
  3. Reduces token usage - Shorter tool names consume fewer AI context tokens
  4. Future-proof - Date-stamped endpoints update without manual renaming
  5. Consistency - Same cleaning logic applied to all tools uniformly

Regenerating Tools

When the API updates, regenerate the tools:

bash
cd pets/my-api
pets generate-openapi

The openapi-client.ts file will be regenerated with the latest endpoints.

Real-World Example: AI Harmony

The pets/ai-harmony pet uses OpenAPI generation with core endpoints:

package.json:

json
{
  "name": "ai-harmony",
  "openapiSpec": {
    "url": "https://ai-harmony.duckdns.org/obsidian/openapi.json",
    "baseUrl": "https://api.obsidian-neural.com",
    "authType": "apiKey",
    "apiKeyHeader": "X-API-Key",
    "coreEndpoints": {
      "tags": ["Generation"],
      "paths": ["/api/v1/generate", "/api/v1/health", "/api/v1/status"]
    }
  }
}

Generate tools:

bash
cd pets/ai-harmony
pets generate-openapi --verbose

Output:

Generated 58 tools to openapi-client.ts
Core tools: 5 (always loaded)
Additional tools: 53 (always loaded)

Additional tool groups included by default:
  AUTHENTICATION: 18 tools
  ADMIN: 25 tools
  GIFTS: 3 tools

Usage:

bash
# Core generation workflow
bun test:pet ai-harmony --query "generate audio with a chill beat at 90 bpm"

# Additional groups are available without extra env vars
bun test:pet ai-harmony --query "list all users"

Troubleshooting

Authentication works in curl but fails in plugin (401 errors)

This is typically caused by environment variables not being loaded before the openapi-client module initializes.

Problem: The generated openapi-client.ts reads process.env.MY_API_KEY at module load time. If your .env file isn't loaded yet, the auth header is built with undefined values.

Solution: The generator now uses lazy authentication - auth headers are built fresh on each request, not cached at module load time:

typescript
// Generated code uses lazy auth evaluation
function getAuthHeaders(): Record<string, string> {
  const headers: Record<string, string> = {
    "Content-Type": "application/json",
    "Accept": "application/json",
  }
  
  // Read env vars at request time, not module load time
  const apiKey = process.env.MY_API_KEY
  if (apiKey) {
    headers["Authorization"] = `Bearer ${apiKey}`
  }
  
  return headers
}

async function fetchAPI(url: string, options: RequestInit): Promise<string> {
  // Get fresh headers on each request
  const authHeaders = getAuthHeaders()
  const response = await fetch(url, { ...options, headers: authHeaders })
  // ...
}

If you have an older generated client, regenerate it:

bash
cd pets/my-api
pets generate-openapi

Debugging steps:

  1. Test credentials directly with curl:
    bash
    source .env && curl -s -u "${MY_API_USER}:${MY_API_PASS}" "https://api.example.com/test"
  2. If curl works but plugin fails, the issue is module load order
  3. Regenerate with the latest generator which uses lazy auth

"No OpenAPI configuration found"

Add openapiSpec to your package.json or use --url/--file options.

"Failed to fetch OpenAPI spec"

  • Check the URL is accessible
  • Verify the spec is valid JSON
  • Try --dump-spec to save the raw spec for debugging

Generated tools have wrong base URL

Specify baseUrl in your config:

json
{
  "openapiSpec": {
    "url": "https://docs.example.com/openapi.json",
    "baseUrl": "https://api.example.com"
  }
}

Missing parameters in generated tools

Check that your OpenAPI spec includes:

  • parameters for path/query/header params
  • requestBody with application/json content for body params

Complex nested objects not working

Due to OpenPets runtime schema limits, deeply nested objects are serialized as JSON strings. Pass them as stringified JSON:

typescript
// Instead of:
{ nested: { a: 1, b: 2 } }

// Pass as:
{ nested: '{"a": 1, "b": 2}' }

Basic auth not working

If using authType: "basic" with separate key and secret:

  1. Ensure you've set both authEnvVar and authSecretEnvVar in your config
  2. Set both environment variables:
    bash
    MY_API_API_KEY=your_key
    MY_API_API_SECRET=your_secret
  3. Regenerate with pets generate-openapi

The generator will create proper Base64 encoding:

typescript
const credentials = `${apiKey}:${apiSecret}`
const encoded = Buffer.from(credentials).toString("base64")

Environment switching not working

If baseUrlEnvVar isn't selecting the correct environment:

  1. Check that altBaseUrl is set in your config
  2. Verify the environment variable matches baseUrlEnvValue exactly:
    bash
    # If baseUrlEnvValue is "live"
    MY_API_ENVIRONMENT=live  # Correct
    MY_API_ENVIRONMENT=LIVE  # Wrong - case sensitive
  3. The default is the inverse of baseUrlEnvValue (if "live" triggers altBaseUrl, "demo" is default)

Rate limiting errors (429)

Many APIs have strict rate limits. The generator returns structured error responses:

json
{
  "success": false,
  "error": "HTTP 429: {\"code\":\"TooManyRequests\"}",
  "status": 429
}

To handle this:

  1. Add delays between requests in your workflows
  2. Check API documentation for specific rate limits
  3. Consider caching responses for read-only endpoints

Real-World Example: Trading 212

The pets/trading-212 pet demonstrates advanced OpenAPI generation features including:

  • Live API spec URL - Always generates from the latest spec
  • Basic auth with separate key/secret
  • Dynamic base URL for demo vs live environments
  • All endpoint tags loaded as core tools

package.json Configuration

json
{
  "name": "@openpets/trading-212",
  "openapiSpec": {
    "url": "https://docs.trading212.com/_bundle/api.json",
    "baseUrl": "https://demo.trading212.com",
    "altBaseUrl": "https://live.trading212.com",
    "baseUrlEnvVar": "TRADING_212_ENVIRONMENT",
    "baseUrlEnvValue": "live",
    "authType": "basic",
    "authEnvVar": "TRADING_212_API_KEY",
    "authSecretEnvVar": "TRADING_212_API_SECRET",
    "coreEndpoints": {
      "tags": ["Accounts", "Positions", "Orders", "Instruments", "Historical events"],
      "paths": ["/api/v0/equity/account", "/api/v0/equity/positions", "/api/v0/equity/orders", "/api/v0/equity/metadata", "/api/v0/equity/history"]
    },
    "exclude": ["Pies (Deprecated)"]
  }
}

Best Practice: Use url instead of file when the API provider hosts their OpenAPI spec. This ensures you always generate from the latest spec version. Run pets generate-openapi periodically to pick up API changes.


### Generated Code Structure

The generator produces:

```typescript
// Dynamic environment selection
const environment = process.env.TRADING_212_ENVIRONMENT || "demo"
const baseUrl = environment === "live" 
  ? "https://live.trading212.com" 
  : "https://demo.trading212.com"

// Separate key and secret for Basic auth
const apiKey = process.env.TRADING_212_API_KEY
const apiSecret = process.env.TRADING_212_API_SECRET

function getBasicAuthHeader(): string | null {
  if (!apiKey || !apiSecret) return null
  const credentials = `${apiKey}:${apiSecret}`
  const encoded = Buffer.from(credentials).toString("base64")
  return `Basic ${encoded}`
}

const authHeader = getBasicAuthHeader()

Environment Variables

bash
# Required
TRADING_212_API_KEY=your_api_key
TRADING_212_API_SECRET=your_api_secret

# Optional - defaults to "demo" for paper trading
TRADING_212_ENVIRONMENT=live

Generated Tools (17 total)

CategoryTools
Accountsget-account-summary
Positionsget-positions
Ordersget-pending-orders, get-order-by-id, cancel-order, place-market-order, place-limit-order, place-stop-order, place-stop-limit-order
Instrumentsget-instruments, get-exchanges
Historicalget-dividends, get-historical-orders, get-transactions, get-reports, request-report

Usage Examples

bash
# Test connection (demo by default)
bun test:pet trading-212 --query "test connection"

# Use live environment for real trading
TRADING_212_ENVIRONMENT=live bun test:pet trading-212 --query "get my account summary"

# Place an order (live)
TRADING_212_ENVIRONMENT=live bun test:pet trading-212 --query "buy 0.1 shares of AAPL_US_EQ"

Key Learnings from Trading 212

  1. Basic Auth with Key+Secret: Many financial APIs use Basic auth with separate key and secret, not a single token. Use authSecretEnvVar to handle this.

  2. Environment Switching: Financial APIs often have demo/sandbox environments. Use altBaseUrl and baseUrlEnvVar for safe testing before live trading.

  3. API-Specific Conventions: Trading 212 requires negative quantity for sell orders and uses specific ticker formats (e.g., AAPL_US_EQ). Document these in your pet's FAQ.

  4. Rate Limiting: The API has strict rate limits (e.g., 1 req/5s for account summary). The generated error handling returns structured error responses including the HTTP status code.

  5. Order by Quantity, Not Value: The Trading 212 API only supports ordering by quantity, not monetary value. Your pet's index.ts can add wrapper tools that calculate quantity from value if needed.

Comparing Generation Methods

FeatureOpenAPIMCPSDK
SourceOpenAPI/Swagger specMCP serverTypeScript SDK
AuthToken in headerOAuth or tokenSDK-specific
SpeedFast (single fetch)Slower (connects to server)Fast (no network)
Schema accuracyFrom specFrom MCP serverFrom config
Body handlingExpanded paramsFlattened paramsJSON string
Best forREST APIsMCP-enabled servicesSDK-first services