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-openapiCLI command with the@src/core/openapi-generator.tstool. 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:
- Ensures Consistency - All OpenAPI pets use the same generation patterns
- Proper Schema Mapping - Handles OpenAPI → Zod type conversion correctly
- OpenPets Integration - Includes read-only mode, core endpoints, auth handling
- Error Handling - Standardized error responses and fetch wrapper
- Future-Proof - Updates to generator benefit all OpenAPI pets
REQUIRED: Use pets generate-openapi CLI
ALWAYS generate OpenAPI tools using the official CLI:
# 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.tsimplementation - Reads your
package.jsonopenapiSpec 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 specificationopenapi-client.ts- Auto-generated tools (200+ lines)- Updated
package.jsonwith openapiSpec configuration - Updated
index.tsto import generated tools
Generated Tools:
commander-test-connection- Test API connectioncommander-list-items- List items with filteringcommander-get-item- Get item by IDcommander-create-item- Create new item
Generation Process:
cd pets/commander
pets generate-openapi --verbose
✅ Generated 4 tools (3 read-only, 1 write) to openapi-client.tsKey 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
| Approach | Best For |
|---|---|
| OpenAPI Generation | REST APIs with OpenAPI specs, API-first products |
| MCP Generation | Services with official MCP servers (Asana, Atlassian, etc.) |
| SDK Generation | Services with TypeScript SDKs (@polar-sh/sdk, etc.) |
| Manual Tools | Custom logic, non-standard APIs, complex workflows |
Quick Start
Option 1: From Command Line
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 --verboseOptional 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:
pets new my-large-api --type openapi --url https://api.example.com/openapi.json --include-firstWhat this does:
- Adds
openapiSpec.includewith 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.includeand runpets generate-openapiagain.
Option 2: From package.json Config (Recommended)
Add an openapiSpec section to your pet's package.json:
{
"name": "@openpets/my-api",
"openapiSpec": {
"url": "https://api.example.com/openapi.json",
"baseUrl": "https://api.example.com"
}
}Then run:
cd pets/my-api
pets generate-openapiBest Practice: Always use
urlwhen 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
| Property | Type | Required | Description |
|---|---|---|---|
url | string | Yes* | URL to fetch OpenAPI spec from |
file | string | Yes* | Local file path to OpenAPI spec |
baseUrl | string | No | Base URL for API calls (overrides spec's servers[0].url) |
altBaseUrl | string | No | Alternative base URL for environment switching (e.g., live vs demo) |
baseUrlEnvVar | string | No | Environment variable to select base URL (e.g., MY_API_ENVIRONMENT) |
baseUrlEnvValue | string | No | Value that triggers altBaseUrl (default: "live") |
authEnvVar | string | No | Environment variable for auth token/key (default: {PET_NAME}_API_KEY) |
authSecretEnvVar | string | No | Environment variable for auth secret (used with Basic auth) |
authType | string | No | Authentication type: bearer, apiKey, basic, or none |
apiKeyHeader | string | No | Header name for API key auth (default: X-API-Key) |
apiKeyIn | string | No | API key location: header, query, or cookie |
apiKeyName | string | No | API key header/query parameter name. Prefer this over apiKeyHeader |
apiKeyValuePrefix | string | No | Optional prefix for API key values, e.g. ShippoToken for Authorization header auth |
responseFormat | string | No | Response format: toon (default, 23.6% token savings), json, or auto - See details |
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 (e.g., ["-controller", "-\\d{4}-\\d{2}-\\d{2}"]) |
coreEndpoints | object | No | Configure which tools are core (always loaded) vs loadEnv (require env var) |
coreEndpoints.tags | string[] | No | Tags whose tools should always be loaded |
coreEndpoints.paths | string[] | No | Path patterns whose tools should always be loaded |
coreEndpoints.operationIds | string[] | No | Operation IDs whose tools should always be loaded |
*Either url or file is required.
CLI Options
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 debuggingGenerated File Structure
The generator creates an openapi-client.ts file with:
- Configuration - Base URL and auth token from environment
- Read-only mode support - Filters write operations when enabled
- Fetch wrapper - Error handling and response parsing
- 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
{
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:
/**
* 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:
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 MyApiPluginSchema Type Mapping
The generator maps OpenAPI/JSON Schema types to Zod types:
| OpenAPI Type | Zod Type |
|---|---|
string | z.string() |
string with enum | z.enum([...]) |
string with format: email | z.string().email() |
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.string() (JSON string 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 with descriptive documentation:
// 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:
requestBody:
content:
application/json:
schema:
type: object
required: [email]
properties:
email:
type: string
name:
type: stringGenerated Schema:
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)
{
"openapiSpec": {
"url": "https://api.example.com/openapi.json",
"authType": "bearer"
}
}Set MY_API_API_KEY in your .env file. The generator adds:
headers["Authorization"] = `Bearer ${apiKey}`API Key Header
{
"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>:
{
"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:
{
"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):
{
"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:
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
{
"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
{
"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
const environment = process.env.MY_API_ENVIRONMENT || "demo"
const baseUrl = environment === "live"
? "https://live.api.example.com"
: "https://demo.api.example.com"Usage
# 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:
pets read-only my-api onWrite 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:
{
"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
- Core tools - Grouped as the most important tools based on
coreEndpointsconfiguration - Additional tools - Generated into named groups and loaded by default
Configuration Example
{
"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/generateor/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:
{
"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 AIai-harmony-generate-audio-test- Test audio generationai-harmony-health-check- Check API healthai-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
- Clear organization - Core tools remain easy to identify in large APIs
- Predictable availability - Agents can use the full generated API without extra setup
- Better metadata - Tiers and groups still help discovery and documentation
- 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:
// 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
{
"openapiSpec": {
"url": "https://api.example.com/openapi.json",
"include": ["users", "projects"]
}
}Exclude Endpoints
{
"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:
{
"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:
- Apply patterns - Each regex pattern is removed from the tool name
- Normalize separators - Multiple consecutive dashes are collapsed to single dashes
- Remove duplicates - Duplicate segments are removed (e.g.,
cal-bookings-bookings-get→cal-bookings-get) - 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:
{
"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 Name | Length |
|---|---|
cal-bookings-controller-2024-08-13-get-bookings | 49 chars |
cal-event-types-controller-2024-06-14-get-event-types | 55 chars |
cal-organizations-attributes-options-controller-get-organization-attribute-assigned-options-by-slug | 99 chars ⚠️ |
After cleaning:
| Cleaned Tool Name | Length |
|---|---|
cal-bookings-get | 16 chars ✅ |
cal-event-types-get | 20 chars ✅ |
cal-org-teams-verified-resources-request-phone-verification-code | 64 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:
{
"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:
{
"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:
cd pets/my-api
pets generate-openapi --verboseOutput 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
- Fits within limits - All tools stay under the runtime's 64-character limit
- Improved readability -
cal-bookings-getis clearer thancal-bookings-controller-2024-08-13-get-bookings - Reduces token usage - Shorter tool names consume fewer AI context tokens
- Future-proof - Date-stamped endpoints update without manual renaming
- Consistency - Same cleaning logic applied to all tools uniformly
Regenerating Tools
When the API updates, regenerate the tools:
cd pets/my-api
pets generate-openapiThe 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:
{
"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:
cd pets/ai-harmony
pets generate-openapi --verboseOutput:
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 toolsUsage:
# 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:
// 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:
cd pets/my-api
pets generate-openapiDebugging steps:
- Test credentials directly with curl:bash
source .env && curl -s -u "${MY_API_USER}:${MY_API_PASS}" "https://api.example.com/test" - If curl works but plugin fails, the issue is module load order
- 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-specto save the raw spec for debugging
Generated tools have wrong base URL
Specify baseUrl in your config:
{
"openapiSpec": {
"url": "https://docs.example.com/openapi.json",
"baseUrl": "https://api.example.com"
}
}Missing parameters in generated tools
Check that your OpenAPI spec includes:
parametersfor path/query/header paramsrequestBodywithapplication/jsoncontent 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:
// 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:
- Ensure you've set both
authEnvVarandauthSecretEnvVarin your config - Set both environment variables:bash
MY_API_API_KEY=your_key MY_API_API_SECRET=your_secret - Regenerate with
pets generate-openapi
The generator will create proper Base64 encoding:
const credentials = `${apiKey}:${apiSecret}`
const encoded = Buffer.from(credentials).toString("base64")Environment switching not working
If baseUrlEnvVar isn't selecting the correct environment:
- Check that
altBaseUrlis set in your config - Verify the environment variable matches
baseUrlEnvValueexactly:bash# If baseUrlEnvValue is "live" MY_API_ENVIRONMENT=live # Correct MY_API_ENVIRONMENT=LIVE # Wrong - case sensitive - 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:
{
"success": false,
"error": "HTTP 429: {\"code\":\"TooManyRequests\"}",
"status": 429
}To handle this:
- Add delays between requests in your workflows
- Check API documentation for specific rate limits
- 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
{
"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
urlinstead offilewhen the API provider hosts their OpenAPI spec. This ensures you always generate from the latest spec version. Runpets generate-openapiperiodically 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
# 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=liveGenerated Tools (17 total)
| Category | Tools |
|---|---|
| Accounts | get-account-summary |
| Positions | get-positions |
| Orders | get-pending-orders, get-order-by-id, cancel-order, place-market-order, place-limit-order, place-stop-order, place-stop-limit-order |
| Instruments | get-instruments, get-exchanges |
| Historical | get-dividends, get-historical-orders, get-transactions, get-reports, request-report |
Usage Examples
# 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
Basic Auth with Key+Secret: Many financial APIs use Basic auth with separate key and secret, not a single token. Use
authSecretEnvVarto handle this.Environment Switching: Financial APIs often have demo/sandbox environments. Use
altBaseUrlandbaseUrlEnvVarfor safe testing before live trading.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.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.
Order by Quantity, Not Value: The Trading 212 API only supports ordering by quantity, not monetary value. Your pet's
index.tscan add wrapper tools that calculate quantity from value if needed.
Comparing Generation Methods
| Feature | OpenAPI | MCP | SDK |
|---|---|---|---|
| Source | OpenAPI/Swagger spec | MCP server | TypeScript SDK |
| Auth | Token in header | OAuth or token | SDK-specific |
| Speed | Fast (single fetch) | Slower (connects to server) | Fast (no network) |
| Schema accuracy | From spec | From MCP server | From config |
| Body handling | Expanded params | Flattened params | JSON string |
| Best for | REST APIs | MCP-enabled services | SDK-first services |